// 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 ref

import (
	"errors"
	"strings"

	"github.com/dolthub/dolt/go/libraries/utils/strhelp"
)

// ErrInvalidRefSpec is the error returned when a refspec isn't syntactically valid
var ErrInvalidRefSpec = errors.New("invalid ref spec")

// ErrInvalidMapping is the error returned when a refspec tries to do an invalid mapping, such as mapping
// refs/heads/main to refs/remotes/origin/*
var ErrInvalidMapping = errors.New("invalid ref spec mapping")

// ErrUnsupportedMapping is returned when trying to do anything other than map local branches (refs/heads/*) to
// remote tracking branches (refs/remotes/*).  As other mappings are added this code will need updating
var ErrUnsupportedMapping = errors.New("unsupported mapping")

// RefSpec is an interface for mapping a reference in one space to a reference in another space.
type RefSpec interface {
	// SrcRef will take a reference to the current working branch and return a reference to what should be used
	// for the source reference of an operation involving a reference spec
	SrcRef(cwbRef DoltRef) DoltRef

	// DestRef will take a source reference and return a reference to what shat should be used for the destination
	// reference of an operation involving a reference spec.
	DestRef(srcRef DoltRef) DoltRef
}

// RemoteRefSpec is an interface that embeds the RefSpec interface and provides an additional method to get the name
// of a remote.
type RemoteRefSpec interface {
	RefSpec
	GetRemote() string
	GetRemRefToLocal() branchMapper
}

// ParseRefSpec parses a RefSpec from a string.
func ParseRefSpec(refSpecStr string) (RefSpec, error) {
	return ParseRefSpecForRemote("", refSpecStr)
}

// ParseRefSpecForRemote takes the name of a remote and a refspec, and parses that refspec, verifying that it refers
// to the appropriate remote
func ParseRefSpecForRemote(remote, refSpecStr string) (RefSpec, error) {
	var fromRef DoltRef
	var toRef DoltRef
	var err error
	if len(refSpecStr) == 0 {
		return nil, ErrInvalidRefSpec
	} else if refSpecStr[0] == ':' {
		fromRef = EmptyBranchRef
		toRef, err = Parse(refSpecStr[1:])

		if err != nil {
			return nil, ErrInvalidRefSpec
		}
	} else {
		tokens := strings.Split(refSpecStr, ":")

		if len(tokens) == 0 {
			return nil, ErrInvalidRefSpec
		}

		srcStr := tokens[0]
		destStr := tokens[0]

		if len(tokens) > 2 {
			return nil, ErrInvalidRefSpec
		} else if len(tokens) == 2 {
			destStr = tokens[1]
		}

		fromRef, err = Parse(srcStr)

		if err != nil {
			return nil, ErrInvalidRefSpec
		}

		toRef, err = Parse(destStr)

		if err != nil {
			return nil, ErrInvalidRefSpec
		}
	}

	if fromRef.GetType() == BranchRefType && toRef.GetType() == RemoteRefType {
		return newLocalToRemoteTrackingRef(remote, fromRef.(BranchRef), toRef.(RemoteRef))
	} else if fromRef.GetType() == BranchRefType && toRef.GetType() == BranchRefType {
		return NewBranchToBranchRefSpec(fromRef.(BranchRef), toRef.(BranchRef))
	} else if fromRef.GetType() == TagRefType && toRef.GetType() == TagRefType {
		return NewTagToTagRefSpec(fromRef.(TagRef), toRef.(TagRef))
	}

	return nil, ErrUnsupportedMapping
}

type branchMapper interface {
	mapBranch(string) string
}

type identityBranchMapper string

func (ibm identityBranchMapper) mapBranch(s string) string {
	return string(ibm)
}

type wcBranchMapper struct {
	prefix string
	suffix string
}

func newWildcardBranchMapper(s string) wcBranchMapper {
	tokens := strings.Split(s, "*")

	if len(tokens) != 2 {
		panic("invalid localPattern")
	}

	return wcBranchMapper{tokens[0], tokens[1]}
}

func (wcbm wcBranchMapper) mapBranch(s string) string {
	return wcbm.prefix + s + wcbm.suffix
}

// BranchToBranchRefSpec maps one branch to another.
type BranchToBranchRefSpec struct {
	srcRef  DoltRef
	destRef DoltRef
}

// NewBranchToBranchRefSpec takes a source and destination BranchRef and returns a RefSpec that maps source to dest.
func NewBranchToBranchRefSpec(srcRef, destRef BranchRef) (RefSpec, error) {
	return BranchToBranchRefSpec{
		srcRef:  srcRef,
		destRef: destRef,
	}, nil
}

// SrcRef will always determine the DoltRef specified as the source ref regardless to the cwbRef
func (rs BranchToBranchRefSpec) SrcRef(cwbRef DoltRef) DoltRef {
	return rs.srcRef
}

// DestRef verifies the localRef matches the refspecs local pattern, and then maps it to a remote tracking branch, or
// nil if it does not match the local pattern.
func (rs BranchToBranchRefSpec) DestRef(r DoltRef) DoltRef {
	if Equals(r, rs.srcRef) {
		return rs.destRef
	}

	return nil
}

type TagToTagRefSpec struct {
	srcRef  DoltRef
	destRef DoltRef
}

// NewTagToTagRefSpec takes a source and destination TagRef and returns a RefSpec that maps source to dest.
func NewTagToTagRefSpec(srcRef, destRef TagRef) (RefSpec, error) {
	return TagToTagRefSpec{
		srcRef:  srcRef,
		destRef: destRef,
	}, nil
}

// SrcRef will always determine the DoltRef specified as the source ref regardless to the cwbRef
func (rs TagToTagRefSpec) SrcRef(_ DoltRef) DoltRef {
	return rs.srcRef
}

// DestRef verifies the localRef matches the refspecs local pattern, and then maps it to a remote tracking branch, or
// nil if it does not match the local pattern.
func (rs TagToTagRefSpec) DestRef(r DoltRef) DoltRef {
	if Equals(r, rs.srcRef) {
		return rs.destRef
	}

	return nil
}

// BranchToTrackingBranchRefSpec maps a branch to the branch that should be tracking it
type BranchToTrackingBranchRefSpec struct {
	localPattern  pattern
	remPattern    pattern
	remote        string
	localToRemRef branchMapper
	remRefToLocal branchMapper
}

func newLocalToRemoteTrackingRef(remote string, srcRef BranchRef, destRef RemoteRef) (RefSpec, error) {
	srcWCs := strings.Count(srcRef.GetPath(), "*")
	destWCs := strings.Count(destRef.GetPath(), "*")

	if srcWCs != destWCs || srcWCs > 1 {
		return nil, ErrInvalidRefSpec
	} else {
		remoteInRef, ok := strhelp.NthToken(destRef.GetPath(), '/', 0)

		if !ok {
			return nil, ErrInvalidRefSpec
		} else if remote != "" && remote != remoteInRef {
			return nil, ErrInvalidRefSpec
		}

		if srcWCs == 0 {
			srcPattern := strPattern(srcRef.GetPath())
			destPattern := strPattern(destRef.GetPath())
			srcToDestMapper := identityBranchMapper(destRef.GetPath()[len(remoteInRef):])
			destToSrcMapper := identityBranchMapper(srcRef.GetPath())

			return BranchToTrackingBranchRefSpec{
				localPattern:  srcPattern,
				remPattern:    destPattern,
				remote:        remoteInRef,
				localToRemRef: srcToDestMapper,
				remRefToLocal: destToSrcMapper,
			}, nil
		} else {
			srcPattern := newWildcardPattern(srcRef.GetPath())
			destPattern := newWildcardPattern(destRef.GetPath())
			srcToDestMapper := newWildcardBranchMapper(destRef.GetPath()[len(remoteInRef)+1:])
			destToSrcMapper := newWildcardBranchMapper(srcRef.GetPath())

			return BranchToTrackingBranchRefSpec{
				localPattern:  srcPattern,
				remPattern:    destPattern,
				remote:        remoteInRef,
				localToRemRef: srcToDestMapper,
				remRefToLocal: destToSrcMapper,
			}, nil
		}
	}
}

// SrcRef will return the current working branch reference that is passed in as long as the cwbRef matches
// the source portion of the ref spec
func (rs BranchToTrackingBranchRefSpec) SrcRef(cwbRef DoltRef) DoltRef {
	if cwbRef.GetType() == BranchRefType {
		_, matches := rs.localPattern.matches(cwbRef.GetPath())
		if matches {
			return cwbRef
		}
	}

	return nil
}

// DestRef verifies the branchRef matches the refspec's local pattern, and then maps it to a remote tracking branch, or
// to nil if it does not match the pattern.
func (rs BranchToTrackingBranchRefSpec) DestRef(branchRef DoltRef) DoltRef {
	if branchRef.GetType() == BranchRefType {
		captured, matches := rs.localPattern.matches(branchRef.GetPath())
		if matches {
			return NewRemoteRef(rs.remote, rs.localToRemRef.mapBranch(captured))
		}
	}

	return nil
}

// GetRemote returns the name of the remote being operated on.
func (rs BranchToTrackingBranchRefSpec) GetRemote() string {
	return rs.remote
}

// GetRemRefToLocal returns the local tracking branch.
func (rs BranchToTrackingBranchRefSpec) GetRemRefToLocal() branchMapper {
	return rs.remRefToLocal
}
