// Copyright (c) 2015, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information

package fdroid

import (
	"encoding/json"
	"encoding/xml"
	"fmt"
	"html"
	"io"
	"sort"
	"strconv"
	"strings"

	"mvdan.cc/fdroidcl/adb"
)

type Index struct {
	Repo     Repo             `json:"repo"`
	Apps     []App            `json:"apps"`
	Packages map[string][]Apk `json:"packages"`
}

type Repo struct {
	Name        string   `json:"name"`
	Timestamp   UnixDate `json:"timestamp"`
	Address     string   `json:"address"`
	Icon        string   `json:"icon"`
	Version     int      `json:"version"`
	MaxAge      int      `json:"maxage"`
	Description string   `json:"description"`
}

// App is an Android application
type App struct {
	PackageName    string   `json:"packageName"`
	Name           string   `json:"name"`
	Summary        string   `json:"summary"`
	Added          UnixDate `json:"added"`
	Updated        UnixDate `json:"lastUpdated"`
	Icon           string   `json:"icon"`
	Description    string   `json:"description"`
	License        string   `json:"license"`
	Categories     []string `json:"categories"`
	Website        string   `json:"webSite"`
	SourceCode     string   `json:"sourceCode"`
	IssueTracker   string   `json:"issueTracker"`
	Changelog      string   `json:"changelog"`
	Donate         string   `json:"donate"`
	Bitcoin        string   `json:"bitcoin"`
	Litecoin       string   `json:"litecoin"`
	FlattrID       string   `json:"flattrID"`
	SugVersName    string   `json:"suggestedVersionName"`
	SugVersCode    int      `json:"suggestedVersionCode,string"`
	FdroidRepoName string   `json:"-"`
	FdroidRepoURL  string   `json:"-"`

	Localized map[string]Localization `json:"localized"`

	Apks []*Apk `json:"-"`
}

type Localization struct {
	Name        string `json:"name"`
	Summary     string `json:"summary"`
	Description string `json:"description"`
}

type IconDensity uint

const (
	UnknownDensity IconDensity = 0
	LowDensity     IconDensity = 120
	MediumDensity  IconDensity = 160
	HighDensity    IconDensity = 240
	XHighDensity   IconDensity = 320
	XXHighDensity  IconDensity = 480
	XXXHighDensity IconDensity = 640
)

func getIconsDir(density IconDensity) string {
	if density == UnknownDensity {
		return "icons"
	}
	for _, d := range [...]IconDensity{
		XXXHighDensity,
		XXHighDensity,
		XHighDensity,
		HighDensity,
		MediumDensity,
	} {
		if density >= d {
			return fmt.Sprintf("icons-%d", d)
		}
	}
	return fmt.Sprintf("icons-%d", LowDensity)
}

func (a *App) IconURLForDensity(density IconDensity) string {
	if len(a.Apks) == 0 {
		return ""
	}
	return fmt.Sprintf("%s/%s/%s", a.Apks[0].RepoURL,
		getIconsDir(density), a.Icon)
}

func (a *App) IconURL() string {
	return a.IconURLForDensity(UnknownDensity)
}

func (a *App) TextDesc(w io.Writer) {
	reader := strings.NewReader(a.Description)
	decoder := xml.NewDecoder(reader)
	firstParagraph := true
	linePrefix := ""
	colsUsed := 0
	var links []string
	linked := false
	for {
		token, err := decoder.Token()
		if err == io.EOF || token == nil {
			break
		}
		switch t := token.(type) {
		case xml.StartElement:
			switch t.Name.Local {
			case "p":
				if firstParagraph {
					firstParagraph = false
				} else {
					fmt.Fprintln(w)
				}
				linePrefix = ""
				colsUsed = 0
			case "li":
				fmt.Fprint(w, "\n *")
				linePrefix = "   "
				colsUsed = 0
			case "a":
				for _, attr := range t.Attr {
					if attr.Name.Local == "href" {
						links = append(links, attr.Value)
						linked = true
						break
					}
				}
			}
		case xml.EndElement:
			switch t.Name.Local {
			case "p", "ul", "ol":
				fmt.Fprintln(w)
			}
		case xml.CharData:
			left := string(t)
			if linked {
				left += fmt.Sprintf("[%d]", len(links)-1)
				linked = false
			}
			limit := 80 - len(linePrefix) - colsUsed
			firstLine := true
			for len(left) > limit {
				last := 0
				for i, c := range left {
					if i >= limit {
						break
					}
					if c == ' ' {
						last = i
					}
				}
				if firstLine {
					firstLine = false
					limit += colsUsed
				} else {
					fmt.Fprint(w, linePrefix)
				}
				fmt.Fprintln(w, left[:last])
				left = left[last+1:]
				colsUsed = 0
			}
			if !firstLine {
				fmt.Fprint(w, linePrefix)
			}
			fmt.Fprint(w, left)
			colsUsed += len(left)
		}
	}
	if len(links) > 0 {
		fmt.Fprintln(w)
		for i, link := range links {
			fmt.Fprintf(w, "[%d] %s\n", i, link)
		}
	}
}

// Apk is an Android package
type Apk struct {
	VersName  string       `json:"versionName"`
	VersCode  int          `json:"versionCode"`
	Size      int64        `json:"size"`
	MinSdk    StringInt    `json:"minSdkVersion"`
	MaxSdk    StringInt    `json:"maxSdkVersion"`
	TargetSdk StringInt    `json:"targetSdkVersion"`
	ABIs      []string     `json:"nativecode"`
	ApkName   string       `json:"apkName"`
	SrcName   string       `json:"srcname"`
	Sig       HexVal       `json:"sig"`
	Signer    HexVal       `json:"signer"`
	Added     UnixDate     `json:"added"`
	Perms     []Permission `json:"uses-permission"`
	Feats     []string     `json:"features"`
	Hash      HexVal       `json:"hash"`
	HashType  string       `json:"hashType"`

	AppID   string `json:"-"`
	RepoURL string `json:"-"`
}

type Permission struct {
	Name   string
	MaxSdk string
}

func (p *Permission) UnmarshalJSON(data []byte) error {
	var v []interface{}
	if err := json.Unmarshal(data, &v); err != nil {
		fmt.Printf("Error while decoding %v\n", err)
		return err
	}
	p.Name = string(v[0].(string))
	if v[1] == nil {
		p.MaxSdk = ""
	} else {
		p.MaxSdk = fmt.Sprint(v[1].(float64))
	}
	return nil
}

// https://codesahara.com/blog/golang-cannot-unmarshal-string-into-go-value-of-type-int/
type StringInt struct {
	Value int
}

func (st *StringInt) UnmarshalJSON(b []byte) error {
	// convert the bytes into an interface
	// this will help us check the type of our value
	// if it is a string that can be converted into an int we convert it
	// otherwise we return an error
	var item interface{}
	if err := json.Unmarshal(b, &item); err != nil {
		return err
	}
	switch v := item.(type) {
	case int:
		*st = StringInt{Value: v}
	case float64:
		*st = StringInt{Value: int(v)}
	case string:
		// here convert the string into an integer
		i, err := strconv.Atoi(v)
		if err != nil {
			// the string might not be of integer type, so return an error
			return err
		}
		*st = StringInt{Value: i}
	}
	return nil
}

func (a *Apk) URL() string {
	return fmt.Sprintf("%s/%s", a.RepoURL, a.ApkName)
}

func (a *Apk) SrcURL() string {
	return fmt.Sprintf("%s/%s", a.RepoURL, a.SrcName)
}

func (a *Apk) IsCompatibleABI(ABIs []string) bool {
	if len(a.ABIs) == 0 {
		return true // APK does not contain native code
	}
	for _, apkABI := range a.ABIs {
		for _, abi := range ABIs {
			if apkABI == abi {
				return true
			}
		}
	}
	return false
}

func (a *Apk) IsCompatibleAPILevel(sdk int) bool {
	return sdk >= a.MinSdk.Value && (a.MaxSdk.Value == 0 || sdk <= a.MaxSdk.Value)
}

func (a *Apk) IsCompatible(device *adb.Device) bool {
	if device == nil {
		return true
	}
	return a.IsCompatibleABI(device.ABIs) &&
		a.IsCompatibleAPILevel(device.APILevel)
}

type AppList []App

func (al AppList) Len() int           { return len(al) }
func (al AppList) Swap(i, j int)      { al[i], al[j] = al[j], al[i] }
func (al AppList) Less(i, j int) bool { return al[i].PackageName < al[j].PackageName }

type ApkList []Apk

func (al ApkList) Len() int           { return len(al) }
func (al ApkList) Swap(i, j int)      { al[i], al[j] = al[j], al[i] }
func (al ApkList) Less(i, j int) bool { return al[i].VersCode > al[j].VersCode }

func LoadIndexJSON(r io.Reader) (*Index, error) {
	var index Index
	decoder := json.NewDecoder(r)
	if err := decoder.Decode(&index); err != nil {
		return nil, err
	}

	sort.Sort(AppList(index.Apps))

	for i := range index.Apps {
		app := &index.Apps[i]
		english, enOK := app.Localized["en"]
		if !enOK {
			english, enOK = app.Localized["en-US"]
		}

		if app.Name == "" && enOK {
			app.Name = english.Name
		}
		// TODO: why does the json index contain html escapes?
		app.Name = html.UnescapeString(app.Name)

		if app.Summary == "" && enOK {
			app.Summary = english.Summary
		}
		if app.Description == "" && enOK {
			app.Description = english.Description
		}
		app.Summary = strings.TrimSpace(app.Summary)
		sort.Sort(ApkList(index.Packages[app.PackageName]))
		for i := range index.Packages[app.PackageName] {
			apk := &index.Packages[app.PackageName][i]
			apk.AppID = app.PackageName
			apk.RepoURL = index.Repo.Address

			// TODO: why does the json index contain html escapes?
			apk.VersName = html.UnescapeString(apk.VersName)

			app.Apks = append(app.Apks, apk)
		}
	}
	return &index, nil
}

func (a *App) SuggestedApk(device *adb.Device) *Apk {
	for _, apk := range a.Apks {
		if a.SugVersCode >= apk.VersCode && apk.IsCompatible(device) {
			return apk
		}
	}
	// fall back to the first compatible apk
	for _, apk := range a.Apks {
		if apk.IsCompatible(device) {
			return apk
		}
	}
	return nil
}
