// Copyright 2018 Google LLC All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//    http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package mutate

import (
	"fmt"

	v1 "github.com/google/go-containerregistry/pkg/v1"
	"github.com/google/go-containerregistry/pkg/v1/empty"
)

// Rebase returns a new v1.Image where the oldBase in orig is replaced by newBase.
func Rebase(orig, oldBase, newBase v1.Image) (v1.Image, error) {
	// Verify that oldBase's layers are present in orig, otherwise orig is
	// not based on oldBase at all.
	origLayers, err := orig.Layers()
	if err != nil {
		return nil, fmt.Errorf("failed to get layers for original: %w", err)
	}
	oldBaseLayers, err := oldBase.Layers()
	if err != nil {
		return nil, err
	}
	if len(oldBaseLayers) > len(origLayers) {
		return nil, fmt.Errorf("image %q is not based on %q (too few layers)", orig, oldBase)
	}
	for i, l := range oldBaseLayers {
		oldLayerDigest, err := l.Digest()
		if err != nil {
			return nil, fmt.Errorf("failed to get digest of layer %d of %q: %w", i, oldBase, err)
		}
		origLayerDigest, err := origLayers[i].Digest()
		if err != nil {
			return nil, fmt.Errorf("failed to get digest of layer %d of %q: %w", i, orig, err)
		}
		if oldLayerDigest != origLayerDigest {
			return nil, fmt.Errorf("image %q is not based on %q (layer %d mismatch)", orig, oldBase, i)
		}
	}

	oldConfig, err := oldBase.ConfigFile()
	if err != nil {
		return nil, fmt.Errorf("failed to get config for old base: %w", err)
	}

	origConfig, err := orig.ConfigFile()
	if err != nil {
		return nil, fmt.Errorf("failed to get config for original: %w", err)
	}

	newConfig, err := newBase.ConfigFile()
	if err != nil {
		return nil, fmt.Errorf("could not get config for new base: %w", err)
	}

	// Stitch together an image that contains:
	// - original image's config
	// - new base image's os/arch properties
	// - new base image's layers + top of original image's layers
	// - new base image's history + top of original image's history
	rebasedImage, err := Config(empty.Image, *origConfig.Config.DeepCopy())
	if err != nil {
		return nil, fmt.Errorf("failed to create empty image with original config: %w", err)
	}

	// Add new config properties from existing images.
	rebasedConfig, err := rebasedImage.ConfigFile()
	if err != nil {
		return nil, fmt.Errorf("could not get config for rebased image: %w", err)
	}
	// OS/Arch properties from new base
	rebasedConfig.Architecture = newConfig.Architecture
	rebasedConfig.OS = newConfig.OS
	rebasedConfig.OSVersion = newConfig.OSVersion

	// Apply config properties to rebased.
	rebasedImage, err = ConfigFile(rebasedImage, rebasedConfig)
	if err != nil {
		return nil, fmt.Errorf("failed to replace config for rebased image: %w", err)
	}

	// Get new base layers and config for history.
	newBaseLayers, err := newBase.Layers()
	if err != nil {
		return nil, fmt.Errorf("could not get new base layers for new base: %w", err)
	}
	// Add new base layers.
	newBaseAdds, err := createAddendums(0, 0, newConfig.History, newBaseLayers)
	if err != nil {
		return nil, fmt.Errorf("failed to process new base image: %w", err)
	}
	rebasedImage, err = Append(rebasedImage, newBaseAdds...)
	if err != nil {
		return nil, fmt.Errorf("failed to append new base image: %w", err)
	}

	// Add original layers above the old base.
	origAdds, err := createAddendums(len(oldConfig.History), len(oldBaseLayers)+1, origConfig.History, origLayers)
	if err != nil {
		return nil, fmt.Errorf("failed to process original image: %w", err)
	}
	rebasedImage, err = Append(rebasedImage, origAdds...)
	if err != nil {
		return nil, fmt.Errorf("failed to append original image: %w", err)
	}

	return rebasedImage, nil
}

// createAddendums makes a list of addendums from a history and layers starting from a specific history and layer
// indexes.
func createAddendums(startHistory, startLayer int, history []v1.History, layers []v1.Layer) ([]Addendum, error) {
	var adds []Addendum
	// History should be a superset of layers; empty layers (e.g. ENV statements) only exist in history.
	// They cannot be iterated identically but must be walked independently, only advancing the iterator for layers
	// when a history entry for a non-empty layer is seen.
	layerIndex := 0
	for historyIndex := range history {
		var layer v1.Layer
		emptyLayer := history[historyIndex].EmptyLayer
		if !emptyLayer {
			if layerIndex >= len(layers) {
				return nil, fmt.Errorf("malformed image: config history references at least %d non-empty layer(s), but only %d layer(s) are present", layerIndex+1, len(layers))
			}
			layer = layers[layerIndex]
			layerIndex++
		}
		if historyIndex >= startHistory || layerIndex >= startLayer {
			adds = append(adds, Addendum{
				Layer:   layer,
				History: history[historyIndex],
			})
		}
	}
	// In the event history was malformed or non-existent, append the remaining layers.
	for i := layerIndex; i < len(layers); i++ {
		if i >= startLayer {
			adds = append(adds, Addendum{Layer: layers[i]})
		}
	}

	return adds, nil
}
