package theme

import (
	"encoding/hex"
	"encoding/json"
	"errors"
	"image/color"
	"io"
	"strings"

	"fyne.io/fyne/v2"
	"fyne.io/fyne/v2/storage"
)

// FromJSON returns a Theme created from the given JSON metadata.
// Any values not present in the data will fall back to the default theme.
// If a parse error occurs it will be returned along with a default theme.
//
// Since: 2.2
func FromJSON(data string) (fyne.Theme, error) {
	return FromJSONReader(strings.NewReader(data))
}

// FromJSONReader returns a Theme created from the given JSON metadata through the reader.
// Any values not present in the data will fall back to the default theme.
// If a parse error occurs it will be returned along with a default theme.
//
// Since: 2.2
func FromJSONReader(r io.Reader) (fyne.Theme, error) {
	return fromJSONWithFallback(r, DefaultTheme())
}

func fromJSONWithFallback(r io.Reader, fallback fyne.Theme) (fyne.Theme, error) {
	var th *schema
	if err := json.NewDecoder(r).Decode(&th); err != nil {
		return fallback, err
	}

	return &jsonTheme{data: th, fallback: fallback}, nil
}

type hexColor string

func (h hexColor) color() (color.Color, error) {
	data := h
	switch len([]rune(h)) {
	case 8, 6:
	case 9, 7: // remove # prefix
		data = h[1:]
	case 5: // remove # prefix, then double up
		data = h[1:]
		fallthrough
	case 4: // could be rgba or #rgb
		if data[0] == '#' {
			v := []rune(data[1:])
			data = hexColor([]rune{v[0], v[0], v[1], v[1], v[2], v[2]})
			break
		}

		v := []rune(data)
		data = hexColor([]rune{v[0], v[0], v[1], v[1], v[2], v[2], v[3], v[3]})
	case 3:
		v := []rune(h)
		data = hexColor([]rune{v[0], v[0], v[1], v[1], v[2], v[2]})
	default:
		return color.Transparent, errors.New("invalid color format: " + string(h))
	}

	digits, err := hex.DecodeString(string(data))
	if err != nil {
		return nil, err
	}
	ret := &color.NRGBA{R: digits[0], G: digits[1], B: digits[2]}
	if len(digits) == 4 {
		ret.A = digits[3]
	} else {
		ret.A = 0xff
	}

	return ret, nil
}

type uriString string

func (u uriString) resource() fyne.Resource {
	uri, err := storage.ParseURI(string(u))
	if err != nil {
		fyne.LogError("Failed to parse URI", err)
		return nil
	}
	r, err := storage.LoadResourceFromURI(uri)
	if err != nil {
		fyne.LogError("Failed to load resource from URI", err)
		return nil
	}
	return r
}

type schema struct {
	Colors      map[string]hexColor `json:"Colors,omitempty"`
	DarkColors  map[string]hexColor `json:"Colors-dark,omitempty"`
	LightColors map[string]hexColor `json:"Colors-light,omitempty"`
	Sizes       map[string]float32  `json:"Sizes,omitempty"`

	Fonts map[string]uriString `json:"Fonts,omitempty"`
	Icons map[string]uriString `json:"Icons,omitempty"`
}

type jsonTheme struct {
	data     *schema
	fallback fyne.Theme
}

func (t *jsonTheme) Color(name fyne.ThemeColorName, variant fyne.ThemeVariant) color.Color {
	switch variant {
	case VariantLight:
		if val, ok := t.data.LightColors[string(name)]; ok {
			c, err := val.color()
			if err != nil {
				fyne.LogError("Failed to parse color", err)
			} else {
				return c
			}
		}
	case VariantDark:
		if val, ok := t.data.DarkColors[string(name)]; ok {
			c, err := val.color()
			if err != nil {
				fyne.LogError("Failed to parse color", err)
			} else {
				return c
			}
		}
	}

	if val, ok := t.data.Colors[string(name)]; ok {
		c, err := val.color()
		if err != nil {
			fyne.LogError("Failed to parse color", err)
		} else {
			return c
		}
	}

	return t.fallback.Color(name, variant)
}

func (t *jsonTheme) Font(style fyne.TextStyle) fyne.Resource {
	if val, ok := t.data.Fonts[styleString(style)]; ok {
		r := val.resource()
		if r != nil {
			return r
		}
	}
	return t.fallback.Font(style)
}

func (t *jsonTheme) Icon(name fyne.ThemeIconName) fyne.Resource {
	if val, ok := t.data.Icons[string(name)]; ok {
		r := val.resource()
		if r != nil {
			return r
		}
	}
	return t.fallback.Icon(name)
}

func (t *jsonTheme) Size(name fyne.ThemeSizeName) float32 {
	if val, ok := t.data.Sizes[string(name)]; ok {
		return val
	}

	return t.fallback.Size(name)
}

func styleString(s fyne.TextStyle) string {
	if s.Bold {
		if s.Italic {
			return "boldItalic"
		}
		return "bold"
	}
	if s.Monospace {
		return "monospace"
	}
	return "regular"
}
