// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge.  If not, see <https://www.gnu.org/licenses/>.

// +build !nogui

package qtie

import (
	"errors"
	"strings"

	qtcommon "github.com/ProtonMail/proton-bridge/internal/frontend/qt-common"
	"github.com/ProtonMail/proton-bridge/internal/transfer"
	"github.com/ProtonMail/proton-bridge/pkg/pmapi"
	"github.com/therecipe/qt/core"
)

const (
	GlobalOptionIndex = -1
)

var AllFolderInfoRoles = []int{
	FolderId,
	FolderName,
	FolderColor,
	FolderType,
	FolderEntries,
	IsFolderSelected,
	FolderFromDate,
	FolderToDate,
	TargetFolderID,
	TargetLabelIDs,
}

func getTargetHashes(mboxes []transfer.Mailbox) (targetFolderID, targetLabelIDs string) {
	for _, targetMailbox := range mboxes {
		if targetMailbox.IsExclusive {
			targetFolderID = targetMailbox.Hash()
		} else {
			targetLabelIDs += targetMailbox.Hash() + ";"
		}
	}

	targetLabelIDs = strings.Trim(targetLabelIDs, ";")
	return
}

func newFolderInfo(mbox transfer.Mailbox, rule *transfer.Rule) *FolderInfo {
	targetFolderID, targetLabelIDs := getTargetHashes(rule.TargetMailboxes)

	entry := &FolderInfo{
		mailbox:          mbox,
		FolderEntries:    1,
		FromDate:         rule.FromTime,
		ToDate:           rule.ToTime,
		IsFolderSelected: rule.Active,
		TargetFolderID:   targetFolderID,
		TargetLabelIDs:   targetLabelIDs,
	}

	entry.FolderType = FolderTypeSystem
	if !pmapi.IsSystemLabel(mbox.ID) {
		if mbox.IsExclusive {
			entry.FolderType = FolderTypeFolder
		} else {
			entry.FolderType = FolderTypeLabel
		}
	}

	return entry
}

func (s *FolderStructure) saveRule(info *FolderInfo) error {
	if s.transfer == nil {
		return errors.New("missing transfer")
	}
	sourceMbox := info.mailbox
	if !info.IsFolderSelected {
		s.transfer.UnsetRule(sourceMbox)
		return nil
	}
	allTargetMboxes, err := s.transfer.TargetMailboxes()
	if err != nil {
		return err
	}
	var targetMboxes []transfer.Mailbox
	for _, target := range allTargetMboxes {
		targetHash := target.Hash()
		if info.TargetFolderID == targetHash || strings.Contains(info.TargetLabelIDs, targetHash) {
			targetMboxes = append(targetMboxes, target)
		}
	}

	return s.transfer.SetRule(sourceMbox, targetMboxes, info.FromDate, info.ToDate)
}

func (s *FolderInfo) updateTargetLabelIDs(targetLabelsSet map[string]struct{}) {
	targets := []string{}
	for key := range targetLabelsSet {
		targets = append(targets, key)
	}
	s.TargetLabelIDs = strings.Join(targets, ";")
}

func (s *FolderInfo) AddTargetLabel(targetID string) {
	if targetID == "" {
		return
	}
	targetLabelsSet := s.getSetOfLabels()
	targetLabelsSet[targetID] = struct{}{}
	s.updateTargetLabelIDs(targetLabelsSet)
}

func (s *FolderInfo) RemoveTargetLabel(targetID string) {
	if targetID == "" {
		return
	}
	targetLabelsSet := s.getSetOfLabels()
	delete(targetLabelsSet, targetID)
	s.updateTargetLabelIDs(targetLabelsSet)
}

func (s *FolderInfo) IsType(askType string) bool {
	return s.FolderType == askType
}

func (s *FolderInfo) getSetOfLabels() (uniqSet map[string]struct{}) {
	uniqSet = make(map[string]struct{})
	for _, label := range s.TargetLabelIDList() {
		uniqSet[label] = struct{}{}
	}
	return
}

func (s *FolderInfo) TargetLabelIDList() []string {
	return strings.FieldsFunc(
		s.TargetLabelIDs,
		func(c rune) bool { return c == ';' },
	)
}

// Get data
func (s *FolderStructure) data(index *core.QModelIndex, role int) *core.QVariant {
	row, isValid := index.Row(), index.IsValid()
	if !isValid || row >= s.getCount() {
		log.Warnln("Wrong index", isValid, row)
		return core.NewQVariant()
	}

	var f = s.get(row)

	switch role {
	case FolderId:
		return qtcommon.NewQVariantString(f.mailbox.Hash())
	case FolderName, int(core.Qt__DisplayRole):
		return qtcommon.NewQVariantString(f.mailbox.Name)
	case FolderColor:
		return qtcommon.NewQVariantString(f.mailbox.Color)
	case FolderType:
		return qtcommon.NewQVariantString(f.FolderType)
	case FolderEntries:
		return qtcommon.NewQVariantInt(f.FolderEntries)
	case FolderFromDate:
		return qtcommon.NewQVariantLong(f.FromDate)
	case FolderToDate:
		return qtcommon.NewQVariantLong(f.ToDate)
	case IsFolderSelected:
		return qtcommon.NewQVariantBool(f.IsFolderSelected)
	case TargetFolderID:
		return qtcommon.NewQVariantString(f.TargetFolderID)
	case TargetLabelIDs:
		return qtcommon.NewQVariantString(f.TargetLabelIDs)
	default:
		log.Warnln("Wrong role", role)
		return core.NewQVariant()
	}
}

// Get header data (table view, tree view)
func (s *FolderStructure) headerData(section int, orientation core.Qt__Orientation, role int) *core.QVariant {
	if role != int(core.Qt__DisplayRole) {
		return core.NewQVariant()
	}

	if orientation == core.Qt__Horizontal {
		return qtcommon.NewQVariantString("Column")
	}

	return qtcommon.NewQVariantString("Row")
}

// Flags is editable
func (s *FolderStructure) flags(index *core.QModelIndex) core.Qt__ItemFlag {
	if !index.IsValid() {
		return core.Qt__ItemIsEnabled
	}

	// can do here also: core.NewQAbstractItemModelFromPointer(s.Pointer()).Flags(index) | core.Qt__ItemIsEditable
	// or s.FlagsDefault(index) | core.Qt__ItemIsEditable
	return core.Qt__ItemIsEnabled | core.Qt__ItemIsSelectable | core.Qt__ItemIsEditable
}

// Set data
func (s *FolderStructure) setData(index *core.QModelIndex, value *core.QVariant, role int) bool {
	log.Debugf("SET DATA %d", role)
	if !index.IsValid() {
		return false
	}
	if index.Row() < GlobalOptionIndex || index.Row() > s.getCount() || index.Column() != 1 {
		return false
	}
	item := s.get(index.Row())
	t := true
	switch role {
	case FolderId, FolderType:
		log.
			WithField("structure", s).
			WithField("row", index.Row()).
			WithField("column", index.Column()).
			WithField("role", role).
			WithField("isEdit", role == int(core.Qt__EditRole)).
			Warn("Set constant role forbiden")
	case FolderName:
		item.mailbox.Name = value.ToString()
	case FolderColor:
		item.mailbox.Color = value.ToString()
	case FolderEntries:
		item.FolderEntries = value.ToInt(&t)
	case FolderFromDate:
		item.FromDate = value.ToLongLong(&t)
	case FolderToDate:
		item.ToDate = value.ToLongLong(&t)
	case IsFolderSelected:
		item.IsFolderSelected = value.ToBool()
	case TargetFolderID:
		item.TargetFolderID = value.ToString()
	case TargetLabelIDs:
		item.TargetLabelIDs = value.ToString()
	default:
		log.Debugln("uknown role ", s, index.Row(), index.Column(), role, role == int(core.Qt__EditRole))
		return false
	}
	s.changedEntityRole(index.Row(), index.Row(), role)
	return true
}

// Dimension of model: number of rows is equivalent to number of items in list
func (s *FolderStructure) rowCount(parent *core.QModelIndex) int {
	return s.getCount()
}

func (s *FolderStructure) getCount() int {
	return len(s.entities)
}

// Returns names of available item properties
func (s *FolderStructure) roleNames() map[int]*core.QByteArray {
	return s.Roles()
}

// Clear removes all items in model
func (s *FolderStructure) Clear() {
	s.BeginResetModel()
	if s.getCount() != 0 {
		s.entities = []*FolderInfo{}
	}

	s.GlobalOptions = FolderInfo{
		mailbox: transfer.Mailbox{
			Name: "=",
		},
		FromDate:       0,
		ToDate:         0,
		TargetFolderID: "",
		TargetLabelIDs: "",
	}
	s.EndResetModel()
}

// Method connected to addEntry slot
func (s *FolderStructure) addEntry(entry *FolderInfo) {
	s.insertEntry(entry, s.getCount())
}

// NewUniqId which is not in map yet.
func (s *FolderStructure) newUniqId() (name string) {
	name = s.GlobalOptions.mailbox.Name
	mbox := transfer.Mailbox{Name: name}
	for newVal := byte(name[0]); true; newVal++ {
		mbox.Name = string([]byte{newVal})
		if s.getRowById(mbox.Hash()) < GlobalOptionIndex {
			return
		}
	}
	return
}

// Method connected to addEntry slot
func (s *FolderStructure) insertEntry(entry *FolderInfo, i int) {
	s.BeginInsertRows(core.NewQModelIndex(), i, i)
	s.entities = append(s.entities[:i], append([]*FolderInfo{entry}, s.entities[i:]...)...)
	s.EndInsertRows()
	// update global if conflict
	if entry.mailbox.Hash() == s.GlobalOptions.mailbox.Hash() {
		globalName := s.newUniqId()
		s.GlobalOptions.mailbox.Name = globalName
	}
}

func (s *FolderStructure) GetInfo(row int) FolderInfo {
	return *s.get(row)
}

func (s *FolderStructure) changedEntityRole(rowStart int, rowEnd int, roles ...int) {
	if rowStart < GlobalOptionIndex || rowEnd < GlobalOptionIndex {
		return
	}
	if rowStart < 0 || rowStart >= s.getCount() {
		rowStart = 0
	}
	if rowEnd < 0 || rowEnd >= s.getCount() {
		rowEnd = s.getCount()
	}
	if rowStart > rowEnd {
		tmp := rowStart
		rowStart = rowEnd
		rowEnd = tmp
	}
	indexStart := s.Index(rowStart, 0, core.NewQModelIndex())
	indexEnd := s.Index(rowEnd, 0, core.NewQModelIndex())
	s.updateSelection(indexStart, indexEnd, roles)
	s.DataChanged(indexStart, indexEnd, roles)
}

func (s *FolderStructure) setFolderSelection(id string, toSelect bool) {
	log.Debugf("set folder selection %q %b", id, toSelect)
	i := s.getRowById(id)
	//
	info := s.get(i)
	before := info.IsFolderSelected
	info.IsFolderSelected = toSelect
	if err := s.saveRule(info); err != nil {
		s.get(i).IsFolderSelected = before
		log.WithError(err).WithField("id", id).WithField("toSelect", toSelect).Error("Cannot set selection")
		return
	}
	//
	s.changedEntityRole(i, i, IsFolderSelected)
}

func (s *FolderStructure) setTargetFolderID(id, target string) {
	log.Debugf("set targetFolderID %q %q", id, target)
	i := s.getRowById(id)
	//
	info := s.get(i)
	//s.get(i).TargetFolderID = target
	before := info.TargetFolderID
	info.TargetFolderID = target
	if err := s.saveRule(info); err != nil {
		info.TargetFolderID = before
		log.WithError(err).WithField("id", id).WithField("target", target).Error("Cannot set target")
		return
	}
	//
	s.changedEntityRole(i, i, TargetFolderID)
	if target == "" { // do not import
		before := info.TargetLabelIDs
		info.TargetLabelIDs = ""
		if err := s.saveRule(info); err != nil {
			info.TargetLabelIDs = before
			log.WithError(err).WithField("id", id).WithField("target", target).Error("Cannot set target")
			return
		}
		s.changedEntityRole(i, i, TargetLabelIDs)
	}
}

func (s *FolderStructure) addTargetLabelID(id, label string) {
	log.Debugf("add target label id %q %q", id, label)
	if label == "" {
		return
	}
	i := s.getRowById(id)
	info := s.get(i)
	before := info.TargetLabelIDs
	info.AddTargetLabel(label)
	if err := s.saveRule(info); err != nil {
		info.TargetLabelIDs = before
		log.WithError(err).WithField("id", id).WithField("label", label).Error("Cannot add label")
		return
	}
	s.changedEntityRole(i, i, TargetLabelIDs)
}

func (s *FolderStructure) removeTargetLabelID(id, label string) {
	log.Debugf("remove label id %q %q", id, label)
	if label == "" {
		return
	}
	i := s.getRowById(id)
	info := s.get(i)
	before := info.TargetLabelIDs
	info.RemoveTargetLabel(label)
	if err := s.saveRule(info); err != nil {
		info.TargetLabelIDs = before
		log.WithError(err).WithField("id", id).WithField("label", label).Error("Cannot remove label")
		return
	}
	s.changedEntityRole(i, i, TargetLabelIDs)
}

func (s *FolderStructure) setFromToDate(id string, from, to int64) {
	log.Debugf("set from to date %q %d %d", id, from, to)
	i := s.getRowById(id)
	info := s.get(i)
	beforeFrom := info.FromDate
	beforeTo := info.ToDate
	info.FromDate = from
	info.ToDate = to
	if err := s.saveRule(info); err != nil {
		info.FromDate = beforeFrom
		info.ToDate = beforeTo
		log.WithError(err).WithField("id", id).WithField("from", from).WithField("to", to).Error("Cannot set date")
		return
	}
	s.changedEntityRole(i, i, FolderFromDate, FolderToDate)
}

func (s *FolderStructure) selectType(folderType string, toSelect bool) {
	log.Debugf("set type %q %b", folderType, toSelect)
	iFirst, iLast := -1, -1
	for i, entity := range s.entities {
		if entity.IsType(folderType) {
			if iFirst == -1 {
				iFirst = i
			}
			before := entity.IsFolderSelected
			entity.IsFolderSelected = toSelect
			if err := s.saveRule(entity); err != nil {
				entity.IsFolderSelected = before
				log.WithError(err).WithField("i", i).WithField("type", folderType).WithField("toSelect", toSelect).Error("Cannot select type")
			}
			iLast = i
		}
	}
	if iFirst != -1 {
		s.changedEntityRole(iFirst, iLast, IsFolderSelected)
	}
}

func (s *FolderStructure) updateSelection(topLeft *core.QModelIndex, bottomRight *core.QModelIndex, roles []int) {
	for _, role := range roles {
		switch role {
		case IsFolderSelected:
			s.SetSelectedFolders(true)
			s.SetSelectedLabels(true)
			s.SetAtLeastOneSelected(false)
			for _, entity := range s.entities {
				if entity.IsFolderSelected {
					s.SetAtLeastOneSelected(true)
				} else {
					if entity.IsType(FolderTypeFolder) {
						s.SetSelectedFolders(false)
					}
					if entity.IsType(FolderTypeLabel) {
						s.SetSelectedLabels(false)
					}
				}
				if !s.IsSelectedFolders() && !s.IsSelectedLabels() && s.IsAtLeastOneSelected() {
					break
				}
			}
		default:
		}
	}
}

func (s *FolderStructure) hasFolderWithName(name string) bool {
	for _, entity := range s.entities {
		if entity.mailbox.Name == name {
			return true
		}
	}
	return false
}

func (s *FolderStructure) getRowById(id string) (row int) {
	for row = GlobalOptionIndex; row < s.getCount(); row++ {
		if id == s.get(row).mailbox.Hash() {
			return
		}
	}
	row = GlobalOptionIndex - 1
	return
}

func (s *FolderStructure) hasTarget() bool {
	for row := 0; row < s.getCount(); row++ {
		if s.get(row).TargetFolderID != "" {
			return true
		}
	}
	return false
}

// Getter for account info pointer
// index out of array length returns empty folder info to avoid segfault
// index == GlobalOptionIndex is set to access global options
func (s *FolderStructure) get(index int) *FolderInfo {
	if index < GlobalOptionIndex || index >= s.getCount() {
		return &FolderInfo{}
	}
	if index == GlobalOptionIndex {
		return &s.GlobalOptions
	}
	return s.entities[index]
}
