// Copyright (c) 2022 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

package mautrix

import (
	"fmt"
	"regexp"
	"strconv"
)

// RespVersions is the JSON response for https://spec.matrix.org/v1.2/client-server-api/#get_matrixclientversions
type RespVersions struct {
	Versions         []SpecVersion   `json:"versions"`
	UnstableFeatures map[string]bool `json:"unstable_features"`
}

func (versions *RespVersions) ContainsFunc(match func(found SpecVersion) bool) bool {
	for _, found := range versions.Versions {
		if match(found) {
			return true
		}
	}
	return false
}

func (versions *RespVersions) Contains(version SpecVersion) bool {
	return versions.ContainsFunc(func(found SpecVersion) bool {
		return found == version
	})
}

func (versions *RespVersions) ContainsGreaterOrEqual(version SpecVersion) bool {
	return versions.ContainsFunc(func(found SpecVersion) bool {
		return found.GreaterThan(version) || found == version
	})
}

func (versions *RespVersions) GetLatest() (latest SpecVersion) {
	for _, ver := range versions.Versions {
		if ver.GreaterThan(latest) {
			latest = ver
		}
	}
	return
}

type UnstableFeature struct {
	UnstableFlag string
	SpecVersion  SpecVersion
}

var (
	FeatureAppservicePing = UnstableFeature{UnstableFlag: "fi.mau.msc2659.stable", SpecVersion: SpecV17}

	BeeperFeatureHungry               = UnstableFeature{UnstableFlag: "com.beeper.hungry"}
	BeeperFeatureBatchSending         = UnstableFeature{UnstableFlag: "com.beeper.batch_sending"}
	BeeperFeatureRoomYeeting          = UnstableFeature{UnstableFlag: "com.beeper.room_yeeting"}
	BeeperFeatureAutojoinInvites      = UnstableFeature{UnstableFlag: "com.beeper.room_create_autojoin_invites"}
	BeeperFeatureArbitraryProfileMeta = UnstableFeature{UnstableFlag: "com.beeper.arbitrary_profile_meta"}
)

func (versions *RespVersions) Supports(feature UnstableFeature) bool {
	return versions.UnstableFeatures[feature.UnstableFlag] ||
		(!feature.SpecVersion.IsEmpty() && versions.ContainsGreaterOrEqual(feature.SpecVersion))
}

type SpecVersionFormat int

const (
	SpecVersionFormatUnknown SpecVersionFormat = iota
	SpecVersionFormatR
	SpecVersionFormatV
)

var (
	SpecR000 = MustParseSpecVersion("r0.0.0")
	SpecR001 = MustParseSpecVersion("r0.0.1")
	SpecR010 = MustParseSpecVersion("r0.1.0")
	SpecR020 = MustParseSpecVersion("r0.2.0")
	SpecR030 = MustParseSpecVersion("r0.3.0")
	SpecR040 = MustParseSpecVersion("r0.4.0")
	SpecR050 = MustParseSpecVersion("r0.5.0")
	SpecR060 = MustParseSpecVersion("r0.6.0")
	SpecR061 = MustParseSpecVersion("r0.6.1")
	SpecV11  = MustParseSpecVersion("v1.1")
	SpecV12  = MustParseSpecVersion("v1.2")
	SpecV13  = MustParseSpecVersion("v1.3")
	SpecV14  = MustParseSpecVersion("v1.4")
	SpecV15  = MustParseSpecVersion("v1.5")
	SpecV16  = MustParseSpecVersion("v1.6")
	SpecV17  = MustParseSpecVersion("v1.7")
)

func (svf SpecVersionFormat) String() string {
	switch svf {
	case SpecVersionFormatR:
		return "r"
	case SpecVersionFormatV:
		return "v"
	default:
		return ""
	}
}

type SpecVersion struct {
	Format SpecVersionFormat
	Major  int
	Minor  int
	Patch  int

	Raw string
}

var legacyVersionRegex = regexp.MustCompile(`^r(\d+)\.(\d+)\.(\d+)$`)
var modernVersionRegex = regexp.MustCompile(`^v(\d+)\.(\d+)$`)

func MustParseSpecVersion(version string) SpecVersion {
	sv, err := ParseSpecVersion(version)
	if err != nil {
		panic(err)
	}
	return sv
}

func ParseSpecVersion(version string) (sv SpecVersion, err error) {
	sv.Raw = version
	if parts := modernVersionRegex.FindStringSubmatch(version); parts != nil {
		sv.Major, _ = strconv.Atoi(parts[1])
		sv.Minor, _ = strconv.Atoi(parts[2])
		sv.Format = SpecVersionFormatV
	} else if parts = legacyVersionRegex.FindStringSubmatch(version); parts != nil {
		sv.Major, _ = strconv.Atoi(parts[1])
		sv.Minor, _ = strconv.Atoi(parts[2])
		sv.Patch, _ = strconv.Atoi(parts[3])
		sv.Format = SpecVersionFormatR
	} else {
		err = fmt.Errorf("version '%s' doesn't match either known syntax", version)
	}
	return
}

func (sv *SpecVersion) UnmarshalText(version []byte) error {
	*sv, _ = ParseSpecVersion(string(version))
	return nil
}

func (sv *SpecVersion) MarshalText() ([]byte, error) {
	return []byte(sv.String()), nil
}

func (sv SpecVersion) String() string {
	switch sv.Format {
	case SpecVersionFormatR:
		return fmt.Sprintf("r%d.%d.%d", sv.Major, sv.Minor, sv.Patch)
	case SpecVersionFormatV:
		return fmt.Sprintf("v%d.%d", sv.Major, sv.Minor)
	default:
		return sv.Raw
	}
}

func (sv SpecVersion) IsEmpty() bool {
	return sv.Format == SpecVersionFormatUnknown && sv.Raw == ""
}

func (sv SpecVersion) LessThan(other SpecVersion) bool {
	return sv != other && !sv.GreaterThan(other)
}

func (sv SpecVersion) GreaterThan(other SpecVersion) bool {
	return sv.Format > other.Format ||
		(sv.Format == other.Format && sv.Major > other.Major) ||
		(sv.Format == other.Format && sv.Major == other.Major && sv.Minor > other.Minor) ||
		(sv.Format == other.Format && sv.Major == other.Major && sv.Minor == other.Minor && sv.Patch > other.Patch)
}
