package rest

import (
	"context"
	"database/sql"
	"fmt"
	"gorm.io/gorm"
	"reflect"
	"strconv"
	"sync"
	"time"
)

var (
	DefaultFormatter = NewFormatter()

	DefaultNullDisplay = ""
)

type FormatFunc func(ctx context.Context, value interface{}, model interface{}, scm *Schema) interface{}

type Formatter struct {
	callbacks sync.Map
}

func (formatter *Formatter) Register(f string, fun FormatFunc) {
	formatter.callbacks.Store(f, fun)
}

func (formatter *Formatter) Format(ctx context.Context, format string, value interface{}, model interface{}, scm *Schema) interface{} {
	v, ok := formatter.callbacks.Load(format)
	if ok {
		return v.(FormatFunc)(ctx, value, model, scm)
	}
	return value
}

func (formatter *Formatter) formatModel(ctx context.Context, val interface{}, schemas []*Schema, stmt *gorm.Statement) interface{} {
	values := make(map[string]interface{})
	refVal := reflect.Indirect(reflect.ValueOf(val))
	for _, field := range schemas {
		f := stmt.Schema.LookUpField(field.Column)
		values[field.Column] = formatter.Format(ctx, field.Format, refVal.FieldByName(f.Name).Interface(), val, field)
	}
	return values
}

func (formatter *Formatter) formatModels(ctx context.Context, val interface{}, schemas []*Schema, stmt *gorm.Statement) interface{} {
	reflectValue := reflect.Indirect(reflect.ValueOf(val))
	if reflectValue.Type().Kind() != reflect.Slice {
		return nil
	}
	length := reflectValue.Len()
	values := make([]interface{}, length)
	for i := 0; i < length; i++ {
		values[i] = formatter.formatModel(ctx, reflectValue.Index(i).Interface(), schemas, stmt)
	}
	return values
}

func stringFormat(ctx context.Context, value interface{}, model interface{}, scm *Schema) interface{} {
	return fmt.Sprint(value)
}

func integerFormat(ctx context.Context, value interface{}, model interface{}, scm *Schema) interface{} {
	var (
		n int
	)
	switch value.(type) {
	case float32, float64:
		n = int(reflect.ValueOf(value).Float())
	case int, int8, int16, int32, int64:
		n = int(reflect.ValueOf(value).Int())
	case uint, uint8, uint16, uint32, uint64:
		n = int(reflect.ValueOf(value).Uint())
	case string:
		n, _ = strconv.Atoi(reflect.ValueOf(value).String())
	case []byte:
		n, _ = strconv.Atoi(string(reflect.ValueOf(value).Bytes()))
	}
	return n
}

func decimalFormat(ctx context.Context, value interface{}, model interface{}, scm *Schema) interface{} {
	var (
		n float64
	)
	switch value.(type) {
	case float32, float64:
		n = float64(reflect.ValueOf(value).Float())
	case int, int8, int16, int32, int64:
		n = float64(reflect.ValueOf(value).Int())
	case uint, uint8, uint16, uint32, uint64:
		n = float64(reflect.ValueOf(value).Uint())
	case string:
		n, _ = strconv.ParseFloat(reflect.ValueOf(value).String(), 64)
	case []byte:
		n, _ = strconv.ParseFloat(string(reflect.ValueOf(value).Bytes()), 64)
	}
	return n
}

func dateFormat(ctx context.Context, value interface{}, model interface{}, scm *Schema) interface{} {
	if t, ok := value.(time.Time); ok {
		return t.Format("2006-01-02")
	}
	if t, ok := value.(*sql.NullTime); ok {
		if t != nil && t.Valid {
			return t.Time.Format("2006-01-02")
		}
	}
	if t, ok := value.(int64); ok {
		tm := time.Unix(t, 0)
		return tm.Format("2006-01-02")
	}
	return DefaultNullDisplay
}

func timeFormat(ctx context.Context, value interface{}, model interface{}, scm *Schema) interface{} {
	if t, ok := value.(time.Time); ok {
		return t.Format("15:04:05")
	}
	if t, ok := value.(*sql.NullTime); ok {
		if t != nil && t.Valid {
			return t.Time.Format("15:04:05")
		}
	}
	if t, ok := value.(int64); ok {
		tm := time.Unix(t, 0)
		return tm.Format("15:04:05")
	}
	return DefaultNullDisplay
}

func datetimeFormat(ctx context.Context, value interface{}, model interface{}, scm *Schema) interface{} {
	if t, ok := value.(time.Time); ok {
		return t.Format("2006-01-02 15:04:05")
	}
	if t, ok := value.(*sql.NullTime); ok {
		if t != nil && t.Valid {
			return t.Time.Format("2006-01-02 15:04:05")
		}
	}
	if t, ok := value.(int64); ok {
		if t > 0 {
			tm := time.Unix(t, 0)
			return tm.Format("2006-01-02 15:04:05")
		}
	}
	return DefaultNullDisplay
}

func durationFormat(ctx context.Context, value interface{}, model interface{}, scm *Schema) interface{} {
	var (
		hour int
		min  int
		sec  int
	)
	n := integerFormat(ctx, value, model, scm).(int)
	hour = n / 3600
	min = (n - hour*3600) / 60
	sec = n - hour*3600 - min*60
	return fmt.Sprintf("%02d:%02d:%02d", hour, min, sec)
}

func dropdownFormat(ctx context.Context, value interface{}, model interface{}, scm *Schema) interface{} {
	attributes := scm.getProperties()
	if attributes != nil && attributes.Values != nil {
		for _, v := range attributes.Values {
			if v.Value == value {
				return v.Label
			}
		}
	}
	return value
}

func NewFormatter() *Formatter {
	formatter := &Formatter{}
	formatter.Register("string", stringFormat)
	formatter.Register("integer", integerFormat)
	formatter.Register("decimal", decimalFormat)
	formatter.Register("date", dateFormat)
	formatter.Register("time", timeFormat)
	formatter.Register("datetime", datetimeFormat)
	formatter.Register("duration", durationFormat)
	formatter.Register("dropdown", dropdownFormat)
	return formatter
}
