package convutil

import (
	"errors"
	"regexp"
	"tinyrdm/backend/types"
	strutil "tinyrdm/backend/utils/string"
)

type DataConvert interface {
	Enable() bool
	Encode(string) (string, bool)
	Decode(string) (string, bool)
}

var (
	jsonConv    JsonConvert
	uniJsonConv UnicodeJsonConvert
	yamlConv    YamlConvert
	xmlConv     XmlConvert
	base64Conv  Base64Convert
	binaryConv  BinaryConvert
	hexConv     HexConvert
	gzipConv    GZipConvert
	deflateConv DeflateConvert
	zstdConv    ZStdConvert
	brotliConv  BrotliConvert
	msgpackConv MsgpackConvert
	phpConv     = NewPhpConvert()
	pickleConv  = NewPickleConvert()
)

var BuildInFormatters = map[string]DataConvert{
	types.FORMAT_JSON:         jsonConv,
	types.FORMAT_UNICODE_JSON: uniJsonConv,
	types.FORMAT_YAML:         yamlConv,
	types.FORMAT_XML:          xmlConv,
	types.FORMAT_HEX:          hexConv,
	types.FORMAT_BINARY:       binaryConv,
}

var BuildInDecoders = map[string]DataConvert{
	types.DECODE_BASE64:  base64Conv,
	types.DECODE_GZIP:    gzipConv,
	types.DECODE_DEFLATE: deflateConv,
	types.DECODE_ZSTD:    zstdConv,
	types.DECODE_BROTLI:  brotliConv,
	types.DECODE_MSGPACK: msgpackConv,
	types.DECODE_PHP:     phpConv,
	types.DECODE_PICKLE:  pickleConv,
}

// ConvertTo convert string to specified type
// @param decodeType empty string indicates automatic detection
// @param formatType empty string indicates automatic detection
// @param custom decoder if any
func ConvertTo(str, decodeType, formatType string, customDecoder []CmdConvert) (value, resultDecode, resultFormat string) {
	if len(str) <= 0 {
		// empty content
		if len(formatType) <= 0 {
			resultFormat = types.FORMAT_RAW
		} else {
			resultFormat = formatType
		}
		if len(decodeType) <= 0 {
			resultDecode = types.DECODE_NONE
		} else {
			resultDecode = decodeType
		}
		return
	}

	// decode first
	value, resultDecode = decodeWith(str, decodeType, customDecoder)
	// then format content
	if len(formatType) <= 0 {
		value, resultFormat = autoViewAs(value)
	} else {
		value, resultFormat = viewAs(value, formatType)
	}
	return
}

func decodeWith(str, decodeType string, customDecoder []CmdConvert) (value, resultDecode string) {
	if len(decodeType) > 0 {
		value = str

		if buildinDecoder, ok := BuildInDecoders[decodeType]; ok {
			if decodedStr, ok := buildinDecoder.Decode(str); ok {
				value = decodedStr
			}
		} else if decodeType != types.DECODE_NONE {
			for _, decoder := range customDecoder {
				if decoder.Name == decodeType {
					if decodedStr, ok := decoder.Decode(str); ok {
						value = decodedStr
					}
					break
				}
			}
		}

		resultDecode = decodeType
		return
	}

	value, resultDecode = autoDecode(str, customDecoder)
	return
}

// attempt try possible decode method
// if no decode is possible, it will return the origin string value and "none" decode type
func autoDecode(str string, customDecoder []CmdConvert) (value, resultDecode string) {
	if len(str) > 0 {
		// pure digit content may incorrect regard as some encoded type, skip decode
		if match, _ := regexp.MatchString(`^\d+$`, str); !match {
			var ok bool
			if len(str)%4 == 0 && len(str) >= 12 && !strutil.IsSameChar(str) {
				if value, ok = base64Conv.Decode(str); ok {
					resultDecode = types.DECODE_BASE64
					return
				}
			}

			if value, ok = gzipConv.Decode(str); ok {
				resultDecode = types.DECODE_GZIP
				return
			}

			// FIXME: skip decompress with deflate due to incorrect format checking
			//if value, ok = decodeDeflate(str); ok {
			//	resultDecode = types.DECODE_DEFLATE
			//	return
			//}

			if value, ok = zstdConv.Decode(str); ok {
				resultDecode = types.DECODE_ZSTD
				return
			}

			// FIXME: skip decompress with brotli due to incorrect format checking
			//if value, ok = decodeBrotli(str); ok {
			//	resultDecode = types.DECODE_BROTLI
			//	return
			//}

			if value, ok = msgpackConv.Decode(str); ok {
				resultDecode = types.DECODE_MSGPACK
				return
			}

			if value, ok = phpConv.Decode(str); ok {
				resultDecode = types.DECODE_PHP
				return
			}

			if value, ok = pickleConv.Decode(str); ok {
				resultDecode = types.DECODE_PICKLE
				return
			}

			// try decode with custom decoder
			for _, decoder := range customDecoder {
				if decoder.Auto {
					if value, ok = decoder.Decode(str); ok {
						resultDecode = decoder.Name
						return
					}
				}
			}
		}
	}

	value = str
	resultDecode = types.DECODE_NONE
	return
}

func viewAs(str, formatType string) (value, resultFormat string) {
	if len(formatType) > 0 {
		value = str
		if buildinFormatter, ok := BuildInFormatters[formatType]; ok {
			if formattedStr, ok := buildinFormatter.Decode(str); ok {
				value = formattedStr
			}
		}
		resultFormat = formatType
		return
	}
	return
}

// attempt automatic convert to possible types
// if no conversion is possible, it will return the origin string value and "plain text" type
func autoViewAs(str string) (value, resultFormat string) {
	if len(str) > 0 {
		var ok bool
		if value, ok = jsonConv.Decode(str); ok {
			resultFormat = types.FORMAT_JSON
			return
		}

		if value, ok = yamlConv.Decode(str); ok {
			resultFormat = types.FORMAT_YAML
			return
		}

		if value, ok = xmlConv.Decode(str); ok {
			resultFormat = types.FORMAT_XML
			return
		}

		if strutil.ContainsBinary(str) {
			if value, ok = hexConv.Decode(str); ok {
				resultFormat = types.FORMAT_HEX
				return
			}
		}
	}

	value = str
	resultFormat = types.FORMAT_RAW
	return
}

func SaveAs(str, format, decode string, customDecoder []CmdConvert) (value string, err error) {
	value = str
	if buildingFormatter, ok := BuildInFormatters[format]; ok {
		if formattedStr, ok := buildingFormatter.Encode(str); ok {
			value = formattedStr
		} else {
			err = errors.New("invalid " + format + " data")
			return
		}
	}

	if buildinDecoder, ok := BuildInDecoders[decode]; ok {
		if encodedValue, ok := buildinDecoder.Encode(str); ok {
			value = encodedValue
		} else {
			err = errors.New("fail to build " + decode)
		}
		return
	} else if decode != types.DECODE_NONE {
		for _, decoder := range customDecoder {
			if decoder.Name == decode {
				if encodedStr, ok := decoder.Encode(str); ok {
					value = encodedStr
				} else {
					err = errors.New("fail to build " + decode)
				}
				return
			}
		}
	}
	return
}
