/*
Real-time Online/Offline Charging System (OCS) for Telecom & ISP environments
Copyright (C) ITsysCOM GmbH

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>
*/

package utils

import (
	"encoding/hex"
	"fmt"
	"math/rand"
	"net"
	"strconv"
	"strings"
	"time"

	"github.com/cgrates/sipingo"
	"github.com/nyaruka/phonenumbers"
)

// DataConverters groups together multiple converters,
// executing optimized conversions
type DataConverters []DataConverter

// ConvertString converts from and to string
func (dcs DataConverters) ConvertString(in string) (out string, err error) {
	outIface := interface{}(in)
	for _, cnv := range dcs {
		if outIface, err = cnv.Convert(outIface); err != nil {
			return
		}
	}
	return IfaceAsString(outIface), nil
}

// DataConverter represents functions which should convert input into output
type DataConverter interface {
	Convert(interface{}) (interface{}, error)
}

// NewDataConverter is a factory of converters
func NewDataConverter(params string) (conv DataConverter, err error) {
	switch {
	case params == MetaDurationSeconds:
		return NewDurationSecondsConverter(EmptyString)
	case params == MetaDurationNanoseconds:
		return NewDurationNanosecondsConverter(EmptyString)
	case strings.HasPrefix(params, MetaRound):
		if len(params) == len(MetaRound) { // no extra params, defaults implied
			return NewRoundConverter(EmptyString)
		}
		return NewRoundConverter(params[len(MetaRound)+1:])
	case strings.HasPrefix(params, MetaMultiply):
		if len(params) == len(MetaMultiply) { // no extra params, defaults implied
			return NewMultiplyConverter(EmptyString)
		}
		return NewMultiplyConverter(params[len(MetaMultiply)+1:])
	case strings.HasPrefix(params, MetaDivide):
		if len(params) == len(MetaDivide) { // no extra params, defaults implied
			return NewDivideConverter(EmptyString)
		}
		return NewDivideConverter(params[len(MetaDivide)+1:])
	case params == MetaDuration:
		return NewDurationConverter(EmptyString)
	case params == MetaIP2Hex:
		return new(IP2HexConverter), nil
	case params == MetaString2Hex:
		return new(String2HexConverter), nil
	case params == MetaSIPURIHost:
		return new(SIPURIHostConverter), nil
	case params == MetaSIPURIUser:
		return new(SIPURIUserConverter), nil
	case params == MetaSIPURIMethod:
		return new(SIPURIMethodConverter), nil
	case params == MetaUnixTime:
		return new(UnixTimeConverter), nil
	case strings.HasPrefix(params, MetaLibPhoneNumber):
		if len(params) == len(MetaLibPhoneNumber) {
			return NewPhoneNumberConverter(EmptyString)
		}
		return NewPhoneNumberConverter(params[len(MetaLibPhoneNumber)+1:])
	case strings.HasPrefix(params, MetaTimeString):
		layout := time.RFC3339
		if len(params) > len(MetaTimeString) { // no extra params, defaults implied
			layout = params[len(MetaTimeString)+1:]
		}
		return NewTimeStringConverter(layout), nil
	case strings.HasPrefix(params, MetaRandom):
		if len(params) == len(MetaRandom) { // no extra params, defaults implied
			return NewRandomConverter(EmptyString)
		}
		return NewRandomConverter(params[len(MetaRandom)+1:])
	default:
		return nil, fmt.Errorf("unsupported converter definition: <%s>", params)
	}
}

func NewDataConverterMustCompile(params string) (conv DataConverter) {
	var err error
	if conv, err = NewDataConverter(params); err != nil {
		panic(fmt.Sprintf("parsing: <%s>, error: %s", params, err.Error()))
	}
	return
}

func NewDurationSecondsConverter(params string) (hdlr DataConverter, err error) {
	return new(DurationSecondsConverter), nil
}

// DurationSecondsConverter converts duration into seconds encapsulated in float64
type DurationSecondsConverter struct{}

func (mS *DurationSecondsConverter) Convert(in interface{}) (
	out interface{}, err error) {
	var inDur time.Duration
	if inDur, err = IfaceAsDuration(in); err != nil {
		return nil, err
	}
	out = inDur.Seconds()
	return
}

func NewDurationNanosecondsConverter(params string) (
	hdlr DataConverter, err error) {
	return new(DurationNanosecondsConverter), nil
}

// DurationNanosecondsConverter converts duration into nanoseconds encapsulated in int64
type DurationNanosecondsConverter struct{}

func (mS *DurationNanosecondsConverter) Convert(in interface{}) (
	out interface{}, err error) {
	var inDur time.Duration
	if inDur, err = IfaceAsDuration(in); err != nil {
		return nil, err
	}
	out = inDur.Nanoseconds()
	return
}

func NewRoundConverter(params string) (hdlr DataConverter, err error) {
	rc := new(RoundConverter)
	var paramsSplt []string
	if params != EmptyString {
		paramsSplt = strings.Split(params, InInFieldSep)
	}
	switch len(paramsSplt) {
	case 0:
		rc.Method = MetaRoundingMiddle
	case 1:
		if rc.Decimals, err = strconv.Atoi(paramsSplt[0]); err != nil {
			return nil, fmt.Errorf("%s converter needs integer as decimals, have: <%s>",
				MetaRound, paramsSplt[0])
		}
		rc.Method = MetaRoundingMiddle
	case 2:
		rc.Method = paramsSplt[1]
		if rc.Decimals, err = strconv.Atoi(paramsSplt[0]); err != nil {
			return nil, fmt.Errorf("%s converter needs integer as decimals, have: <%s>",
				MetaRound, paramsSplt[0])
		}
	default:
		return nil, fmt.Errorf("unsupported %s converter parameters: <%s>",
			MetaRound, params)
	}
	return rc, nil
}

// RoundConverter will round floats
type RoundConverter struct {
	Decimals int
	Method   string
}

func (rnd *RoundConverter) Convert(in interface{}) (out interface{}, err error) {
	var inFloat float64
	if inFloat, err = IfaceAsFloat64(in); err != nil {
		return
	}
	out = Round(inFloat, rnd.Decimals, rnd.Method)
	return
}

func NewMultiplyConverter(constructParams string) (hdlr DataConverter, err error) {
	if constructParams == EmptyString {
		return nil, ErrMandatoryIeMissingNoCaps
	}
	var val float64
	if val, err = strconv.ParseFloat(constructParams, 64); err != nil {
		return
	}
	return &MultiplyConverter{Value: val}, nil
}

// MultiplyConverter multiplies input with value in params
// encapsulates the output as float64 value
type MultiplyConverter struct {
	Value float64
}

func (m *MultiplyConverter) Convert(in interface{}) (out interface{}, err error) {
	var inFloat64 float64
	if inFloat64, err = IfaceAsFloat64(in); err != nil {
		return nil, err
	}
	out = inFloat64 * m.Value
	return
}

func NewDivideConverter(constructParams string) (hdlr DataConverter, err error) {
	if constructParams == EmptyString {
		return nil, ErrMandatoryIeMissingNoCaps
	}
	var val float64
	if val, err = strconv.ParseFloat(constructParams, 64); err != nil {
		return
	}
	return &DivideConverter{Value: val}, nil
}

// DivideConverter divides input with value in params
// encapsulates the output as float64 value
type DivideConverter struct {
	Value float64
}

func (m *DivideConverter) Convert(in interface{}) (out interface{}, err error) {
	var inFloat64 float64
	if inFloat64, err = IfaceAsFloat64(in); err != nil {
		return nil, err
	}
	out = inFloat64 / m.Value
	return
}

func NewDurationConverter(params string) (hdlr DataConverter, err error) {
	return new(DurationConverter), nil
}

// DurationConverter converts duration into seconds encapsulated in float64
type DurationConverter struct{}

func (mS *DurationConverter) Convert(in interface{}) (
	out interface{}, err error) {
	return IfaceAsDuration(in)
}

// NewPhoneNumberConverter create a new phoneNumber converter
// If the format isn't specify by default we use NATIONAL
// Possible fromats are : E164(0) , INTERNATIONAL(1) , NATIONAL(2) ,RFC3966(3)
// Also ContryCode needs to be specified
func NewPhoneNumberConverter(params string) (
	pbDC DataConverter, err error) {
	lc := new(PhoneNumberConverter)
	var paramsSplt []string
	if params != EmptyString {
		paramsSplt = strings.Split(params, InInFieldSep)
	}
	switch len(paramsSplt) {
	case 2:
		lc.CountryCode = paramsSplt[0]
		frm, err := strconv.Atoi(paramsSplt[1])
		if err != nil {
			return nil, err
		}
		lc.Format = phonenumbers.PhoneNumberFormat(frm)
	case 1:
		lc.CountryCode = paramsSplt[0]
		lc.Format = 2
	default:
		return nil, fmt.Errorf("unsupported %s converter parameters: <%s>",
			MetaLibPhoneNumber, params)
	}
	return lc, nil
}

// PhoneNumberConverter converts
type PhoneNumberConverter struct {
	CountryCode string
	Format      phonenumbers.PhoneNumberFormat
}

func (lc *PhoneNumberConverter) Convert(in interface{}) (out interface{}, err error) {
	num, err := phonenumbers.Parse(IfaceAsString(in), lc.CountryCode)
	if err != nil {
		return nil, err
	}
	return phonenumbers.Format(num, lc.Format), nil
}

// IP2HexConverter will transform ip to hex
type IP2HexConverter struct{}

// Convert implements DataConverter interface
func (*IP2HexConverter) Convert(in interface{}) (out interface{}, err error) {
	var ip net.IP
	switch val := in.(type) {
	case string:
		ip = net.ParseIP(val)
	case net.IP:
		ip = val
	default:
		src := IfaceAsString(in)
		ip = net.ParseIP(src)
	}

	hx := hex.EncodeToString([]byte(ip))
	if len(hx) < 8 {
		return hx, nil
	}
	return "0x" + string([]byte(hx)[len(hx)-8:]), nil
}

// SIPURIHostConverter will return the
type SIPURIHostConverter struct{}

// Convert implements DataConverter interface
func (*SIPURIHostConverter) Convert(in interface{}) (out interface{}, err error) {
	val := IfaceAsString(in)
	return sipingo.HostFrom(val), nil
}

// SIPURIUserConverter will return the
type SIPURIUserConverter struct{}

// Convert implements DataConverter interface
func (*SIPURIUserConverter) Convert(in interface{}) (out interface{}, err error) {
	val := IfaceAsString(in)
	return sipingo.UserFrom(val), nil
}

// SIPURIMethodConverter will return the
type SIPURIMethodConverter struct{}

// Convert implements DataConverter interface
func (*SIPURIMethodConverter) Convert(in interface{}) (out interface{}, err error) {
	val := IfaceAsString(in)
	return sipingo.MethodFrom(val), nil
}

func NewTimeStringConverter(params string) (hdlr DataConverter) {
	return &TimeStringConverter{Layout: params}
}

type TimeStringConverter struct {
	Layout string
}

// Convert implements DataConverter interface
func (tS *TimeStringConverter) Convert(in interface{}) (
	out interface{}, err error) {
	tm, err := ParseTimeDetectLayout(in.(string), EmptyString)
	if err != nil {
		return nil, err
	}
	return tm.Format(tS.Layout), nil
}

// String2HexConverter will transform the string to hex
type String2HexConverter struct{}

// Convert implements DataConverter interface
func (*String2HexConverter) Convert(in interface{}) (o interface{}, err error) {
	var out string
	if out = hex.EncodeToString([]byte(IfaceAsString(in))); len(out) == 0 {
		o = out
		return
	}
	o = "0x" + out
	return
}

// UnixTimeConverter converts the interface in the unix time
type UnixTimeConverter struct{}

// Convert implements DataConverter interface
func (tS *UnixTimeConverter) Convert(in interface{}) (
	out interface{}, err error) {
	var tm time.Time
	if tm, err = ParseTimeDetectLayout(in.(string), EmptyString); err != nil {
		return
	}
	out = tm.Unix()
	return
}

func NewRandomConverter(params string) (dflr DataConverter, err error) {
	randConv := &RandomConverter{}
	if params == EmptyString {
		dflr = randConv
		return
	}
	sls := strings.Split(params, InInFieldSep)
	switch len(sls) {
	case 2:
		if sls[0] != EmptyString {
			if randConv.begin, err = strconv.Atoi(sls[0]); err != nil {
				return
			}
		}
		if sls[1] != EmptyString {
			if randConv.end, err = strconv.Atoi(sls[1]); err != nil {
				return
			}
		}
	case 1:
		if randConv.begin, err = strconv.Atoi(sls[0]); err != nil {
			return
		}
	}
	dflr = randConv
	return
}

type RandomConverter struct {
	begin, end int
}

// Convert implements DataConverter interface
func (rC *RandomConverter) Convert(in interface{}) (
	out interface{}, err error) {
	if rC.begin == 0 {
		if rC.end == 0 {
			return rand.Int(), nil
		} else {
			return rand.Intn(rC.end), nil
		}
	} else {
		if rC.end == 0 {
			return rand.Int() + rC.begin, nil
		} else {
			return RandomInteger(rC.begin, rC.end), nil
		}
	}
}
