// Copyright 2014 Team 254. All Rights Reserved.
// Author: pat@patfairbank.com (Patrick Fairbank)
//
// Functions for creating and updating the elimination match schedule.

package tournament

import (
	"fmt"
	"github.com/Team254/cheesy-arena-lite/model"
	"strconv"
	"time"
)

const ElimMatchSpacingSec = 600

// Incrementally creates any elimination matches that can be created, based on the results of alliance
// selection or prior elimination rounds. Returns true if the tournament is won.
func UpdateEliminationSchedule(database *model.Database, startTime time.Time) (bool, error) {
	alliances, err := database.GetAllAlliances()
	if err != nil {
		return false, err
	}
	winner, err := buildEliminationMatchSet(database, 1, 1, len(alliances))
	if err != nil {
		return false, err
	}

	// Update the scheduled time for all matches that have yet to be run.
	matches, err := database.GetMatchesByType("elimination")
	if err != nil {
		return false, err
	}
	matchIndex := 0
	for _, match := range matches {
		if match.IsComplete() {
			continue
		}
		match.Time = startTime.Add(time.Duration(matchIndex*ElimMatchSpacingSec) * time.Second)
		if err = database.UpdateMatch(&match); err != nil {
			return false, err
		}
		matchIndex++
	}

	return len(winner) > 0, err
}

// Updates the alliance, if necessary, to include whoever played in the match, in case there was a substitute.
func UpdateAlliance(database *model.Database, matchTeamIds [3]int, allianceId int) error {
	allianceTeams, err := database.GetTeamsByAlliance(allianceId)
	if err != nil {
		return err
	}

	for _, teamId := range matchTeamIds {
		found := false
		for _, allianceTeam := range allianceTeams {
			if teamId == allianceTeam.TeamId {
				found = true
				break
			}
		}
		if !found {
			newAllianceTeam := model.AllianceTeam{AllianceId: allianceId, PickPosition: len(allianceTeams),
				TeamId: teamId}
			allianceTeams = append(allianceTeams, newAllianceTeam)
			if err := database.CreateAllianceTeam(&newAllianceTeam); err != nil {
				return err
			}
		}
	}

	return nil
}

// Recursively traverses the elimination bracket downwards, creating matches as necessary. Returns the winner
// of the given round if known.
func buildEliminationMatchSet(database *model.Database, round int, group int,
	numAlliances int) ([]model.AllianceTeam, error) {
	if numAlliances < 2 {
		return []model.AllianceTeam{}, fmt.Errorf("Must have at least 2 alliances")
	}
	roundName, ok := model.ElimRoundNames[round]
	if !ok {
		return []model.AllianceTeam{}, fmt.Errorf("Round of depth %d is not supported", round*2)
	}
	if round != 1 {
		roundName += strconv.Itoa(group)
	}

	// Recurse to figure out who the involved alliances are.
	var redAlliance, blueAlliance []model.AllianceTeam
	var err error
	if numAlliances < 4*round {
		// This is the first round for some or all alliances and will be at least partially populated from the
		// alliance selection results.
		matchups := []int{1, 16, 8, 9, 4, 13, 5, 12, 2, 15, 7, 10, 3, 14, 6, 11}
		factor := len(matchups) / round
		redAllianceNumber := matchups[(group-1)*factor]
		blueAllianceNumber := matchups[(group-1)*factor+factor/2]
		numDirectAlliances := 4*round - numAlliances
		if redAllianceNumber <= numDirectAlliances {
			// The red alliance has a bye or the number of alliances is a power of 2; get from alliance selection.
			redAlliance, err = database.GetTeamsByAlliance(redAllianceNumber)
			if err != nil {
				return []model.AllianceTeam{}, err
			}

			if len(redAlliance) >= 3 {
				// Swap the teams around to match the positions dictated by the rules.
				redAlliance[0], redAlliance[1], redAlliance[2] = redAlliance[1], redAlliance[0], redAlliance[2]
			}
		}
		if blueAllianceNumber <= numDirectAlliances {
			// The blue alliance has a bye or the number of alliances is a power of 2; get from alliance selection.
			blueAlliance, err = database.GetTeamsByAlliance(blueAllianceNumber)
			if err != nil {
				return []model.AllianceTeam{}, err
			}

			if len(blueAlliance) >= 3 {
				// Swap the teams around to match the positions dictated by the rules.
				blueAlliance[0], blueAlliance[1], blueAlliance[2] = blueAlliance[1], blueAlliance[0], blueAlliance[2]
			}
		}
	}

	// If the alliances aren't known yet, get them from one round down in the bracket.
	if len(redAlliance) == 0 {
		redAlliance, err = buildEliminationMatchSet(database, round*2, group*2-1, numAlliances)
		if err != nil {
			return []model.AllianceTeam{}, err
		}
	}
	if len(blueAlliance) == 0 {
		blueAlliance, err = buildEliminationMatchSet(database, round*2, group*2, numAlliances)
		if err != nil {
			return []model.AllianceTeam{}, err
		}
	}

	// Bail if the rounds below are not yet complete and we don't know either alliance competing this round.
	if len(redAlliance) == 0 && len(blueAlliance) == 0 {
		return []model.AllianceTeam{}, nil
	}

	// Check if the match set exists already and if it has been won.
	var redWins, blueWins, numIncomplete int
	var ties []model.Match
	matches, err := database.GetMatchesByElimRoundGroup(round, group)
	if err != nil {
		return []model.AllianceTeam{}, err
	}
	var unplayedMatches []model.Match
	for _, match := range matches {
		if !match.IsComplete() {
			// Update the teams in the match if they are not yet set or are incorrect.
			if len(redAlliance) != 0 && !(match.Red1 == redAlliance[0].TeamId && match.Red2 == redAlliance[1].TeamId &&
				match.Red3 == redAlliance[2].TeamId) {
				positionRedTeams(&match, redAlliance)
				match.ElimRedAlliance = redAlliance[0].AllianceId
				if err = database.UpdateMatch(&match); err != nil {
					return nil, err
				}
			}
			if len(blueAlliance) != 0 && !(match.Blue1 == blueAlliance[0].TeamId &&
				match.Blue2 == blueAlliance[1].TeamId && match.Blue3 == blueAlliance[2].TeamId) {
				positionBlueTeams(&match, blueAlliance)
				match.ElimBlueAlliance = blueAlliance[0].AllianceId
				if err = database.UpdateMatch(&match); err != nil {
					return nil, err
				}
			}

			unplayedMatches = append(unplayedMatches, match)
			numIncomplete += 1
			continue
		}

		// Reorder the teams based on the last complete match, so that new and unplayed matches use the same positions.
		err = reorderTeams(match.Red1, match.Red2, match.Red3, redAlliance)
		if err != nil {
			return []model.AllianceTeam{}, err
		}
		err = reorderTeams(match.Blue1, match.Blue2, match.Blue3, blueAlliance)
		if err != nil {
			return []model.AllianceTeam{}, err
		}

		// Check who won.
		switch match.Status {
		case model.RedWonMatch:
			redWins += 1
		case model.BlueWonMatch:
			blueWins += 1
		case model.TieMatch:
			ties = append(ties, match)
		default:
			return []model.AllianceTeam{}, fmt.Errorf("Completed match %d has invalid winner '%s'", match.Id,
				match.Status)
		}
	}

	// Delete any superfluous matches if the round is won.
	if redWins == 2 || blueWins == 2 {
		for _, match := range unplayedMatches {
			err = database.DeleteMatch(match.Id)
			if err != nil {
				return []model.AllianceTeam{}, err
			}
		}

		// Bail out and announce the winner of this round.
		if redWins == 2 {
			return redAlliance, nil
		} else {
			return blueAlliance, nil
		}
	}

	// Create initial set of matches or recreate any superfluous matches that were deleted but now are needed
	// due to a revision in who won.
	if len(matches) == 0 || len(ties) == 0 && numIncomplete == 0 {
		// Fill in zeroes if only one alliance is known.
		if len(redAlliance) == 0 {
			redAlliance = []model.AllianceTeam{{}, {}, {}}
		} else if len(blueAlliance) == 0 {
			blueAlliance = []model.AllianceTeam{{}, {}, {}}
		}
		if len(redAlliance) < 3 || len(blueAlliance) < 3 {
			// Raise an error if the alliance selection process gave us less than 3 teams per alliance.
			return []model.AllianceTeam{}, fmt.Errorf("Alliances must consist of at least 3 teams")
		}
		if len(matches) < 1 {
			err = database.CreateMatch(createMatch(roundName, round, group, 1, redAlliance, blueAlliance))
			if err != nil {
				return []model.AllianceTeam{}, err
			}
		}
		if len(matches) < 2 {
			err = database.CreateMatch(createMatch(roundName, round, group, 2, redAlliance, blueAlliance))
			if err != nil {
				return []model.AllianceTeam{}, err
			}
		}
		if len(matches) < 3 {
			err = database.CreateMatch(createMatch(roundName, round, group, 3, redAlliance, blueAlliance))
			if err != nil {
				return []model.AllianceTeam{}, err
			}
		}
	}

	// Duplicate any ties if we have run out of matches.
	if numIncomplete == 0 {
		for index := range ties {
			err = database.CreateMatch(createMatch(roundName, round, group, len(matches)+index+1, redAlliance,
				blueAlliance))
			if err != nil {
				return []model.AllianceTeam{}, err
			}
		}
	}

	return []model.AllianceTeam{}, nil
}

// Creates a match at the given point in the elimination bracket and populates the teams.
func createMatch(roundName string, round int, group int, instance int, redAlliance,
	blueAlliance []model.AllianceTeam) *model.Match {
	match := model.Match{Type: "elimination", DisplayName: fmt.Sprintf("%s-%d", roundName, instance),
		ElimRound: round, ElimGroup: group, ElimInstance: instance, ElimRedAlliance: redAlliance[0].AllianceId,
		ElimBlueAlliance: blueAlliance[0].AllianceId}
	positionRedTeams(&match, redAlliance)
	positionBlueTeams(&match, blueAlliance)
	return &match
}

// Assigns the first three teams from the alliance into the red team slots for the match.
func positionRedTeams(match *model.Match, alliance []model.AllianceTeam) {
	match.Red1 = alliance[0].TeamId
	match.Red2 = alliance[1].TeamId
	match.Red3 = alliance[2].TeamId
}

// Assigns the first three teams from the alliance into the blue team slots for the match.
func positionBlueTeams(match *model.Match, alliance []model.AllianceTeam) {
	match.Blue1 = alliance[0].TeamId
	match.Blue2 = alliance[1].TeamId
	match.Blue3 = alliance[2].TeamId
}

// Swaps the order of teams in the alliance to match the match positioning.
func reorderTeams(team1, team2, team3 int, alliance []model.AllianceTeam) error {
	for i, team := range []int{team1, team2, team3} {
		found := false
		for j, oldTeam := range alliance {
			if team == oldTeam.TeamId {
				alliance[i], alliance[j] = alliance[j], alliance[i]
				found = true
				break
			}
		}
		if !found {
			return fmt.Errorf("Team %d not found in alliance %v", team, alliance)
		}
	}

	return nil
}
