// Copyright 2019 Dolthub, Inc.
//
// 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 diff

import (
	"reflect"

	"github.com/dolthub/dolt/go/libraries/doltcore/doltdb"
	"github.com/dolthub/dolt/go/libraries/doltcore/schema"
)

type SchemaChangeType int

const (
	// SchDiffNone is the SchemaChangeType for two columns with the same tag that are identical
	SchDiffNone SchemaChangeType = iota
	// SchDiffAdded is the SchemaChangeType when a column is in the new schema but not the old
	SchDiffAdded
	// SchDiffRemoved is the SchemaChangeType when a column is in the old schema but not the new
	SchDiffRemoved
	// SchDiffModified is the SchemaChangeType for two columns with the same tag that are different
	SchDiffModified
)

// ColumnDifference is the result of comparing two columns from two schemas.
type ColumnDifference struct {
	DiffType SchemaChangeType
	Tag      uint64
	Old      *schema.Column
	New      *schema.Column
}

type columnPair [2]*schema.Column

// DiffSchColumns compares two schemas by looking at columns with the same tag.
func DiffSchColumns(fromSch, toSch schema.Schema) (map[uint64]ColumnDifference, []uint64) {
	colPairMap, unionTags := pairColumns(fromSch, toSch)

	diffs := make(map[uint64]ColumnDifference)
	for _, tag := range unionTags {
		colPair := colPairMap[tag]
		if colPair[0] == nil {
			diffs[tag] = ColumnDifference{SchDiffAdded, tag, nil, colPair[1]}
		} else if colPair[1] == nil {
			diffs[tag] = ColumnDifference{SchDiffRemoved, tag, colPair[0], nil}
		} else if !colPair[0].Equals(*colPair[1]) {
			diffs[tag] = ColumnDifference{SchDiffModified, tag, colPair[0], colPair[1]}
		} else {
			diffs[tag] = ColumnDifference{SchDiffNone, tag, colPair[0], colPair[1]}
		}
	}

	return diffs, unionTags
}

// pairColumns loops over both sets of columns pairing columns with the same tag.
func pairColumns(fromSch, toSch schema.Schema) (map[uint64]columnPair, []uint64) {
	// collect the tag union of the two schemas, ordering fromSch before toSch
	var unionTags []uint64
	colPairMap := make(map[uint64]columnPair)

	_ = fromSch.GetAllCols().Iter(func(tag uint64, col schema.Column) (stop bool, err error) {
		colPairMap[tag] = columnPair{&col, nil}
		unionTags = append(unionTags, tag)

		return false, nil
	})

	_ = toSch.GetAllCols().Iter(func(tag uint64, col schema.Column) (stop bool, err error) {
		if pair, ok := colPairMap[tag]; ok {
			pair[1] = &col
			colPairMap[tag] = pair
		} else {
			colPairMap[tag] = columnPair{nil, &col}
			unionTags = append(unionTags, tag)
		}

		return false, nil
	})

	return colPairMap, unionTags
}

type IndexDifference struct {
	DiffType SchemaChangeType
	From     schema.Index
	To       schema.Index
}

// DiffSchIndexes matches two sets of Indexes based on column definitions.
// It returns matched and unmatched Indexes as a slice of IndexDifferences.
func DiffSchIndexes(fromSch, toSch schema.Schema) (diffs []IndexDifference) {
	_ = fromSch.Indexes().Iter(func(fromIdx schema.Index) (stop bool, err error) {
		toIdx, ok := toSch.Indexes().GetIndexByTags(fromIdx.IndexedColumnTags()...)

		if !ok {
			diffs = append(diffs, IndexDifference{
				DiffType: SchDiffRemoved,
				From:     fromIdx,
			})
			return false, nil
		}

		d := IndexDifference{
			DiffType: SchDiffModified,
			From:     fromIdx,
			To:       toIdx,
		}

		if fromIdx.Equals(toIdx) {
			d.DiffType = SchDiffNone
		}
		diffs = append(diffs, d)

		return false, nil
	})

	_ = toSch.Indexes().Iter(func(toIdx schema.Index) (stop bool, err error) {
		// if we've seen this index, skip
		for _, d := range diffs {
			if d.To != nil && d.To.Equals(toIdx) {
				return false, nil
			}
		}

		diffs = append(diffs, IndexDifference{
			DiffType: SchDiffAdded,
			To:       toIdx,
		})

		return false, nil
	})

	return diffs
}

type ForeignKeyDifference struct {
	DiffType SchemaChangeType
	From     doltdb.ForeignKey
	To       doltdb.ForeignKey
}

// DiffForeignKeys matches two sets of ForeignKeys based on column definitions.
// It returns matched and unmatched ForeignKeys as a slice of ForeignKeyDifferences.
func DiffForeignKeys(fromFks, toFKs []doltdb.ForeignKey) (diffs []ForeignKeyDifference) {
	for _, from := range fromFks {
		matched := false
		for _, to := range toFKs {
			if reflect.DeepEqual(from.ReferencedTableColumns, to.ReferencedTableColumns) &&
				reflect.DeepEqual(from.TableColumns, to.TableColumns) {

				matched = true
				d := ForeignKeyDifference{
					DiffType: SchDiffModified,
					From:     from,
					To:       to,
				}

				if from.DeepEquals(to) {
					d.DiffType = SchDiffNone
				}
				diffs = append(diffs, d)

				break
			}
		}

		if !matched {
			diffs = append(diffs, ForeignKeyDifference{
				DiffType: SchDiffRemoved,
				From:     from,
			})
		}
	}

	for _, to := range toFKs {
		seen := false
		for _, d := range diffs {
			if d.To.DeepEquals(to) {
				seen = true
				break
			}
		}
		if seen {
			continue
		}

		diffs = append(diffs, ForeignKeyDifference{
			DiffType: SchDiffAdded,
			To:       to,
		})
	}
	return diffs
}
