package dbc

import (
	"encoding/base64"
	"math"
	"strings"

	"github.com/fiskerinc/cloud-services/pkg/common"
	"github.com/fiskerinc/cloud-services/pkg/dbc/diagnostics"
	"github.com/fiskerinc/cloud-services/pkg/dbc/state"
	"github.com/fiskerinc/cloud-services/pkg/grpc/kafka_grpc"

	can "github.com/fiskerinc/cloud-services/pkg/can-go"
	"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/descriptor"

	"github.com/pkg/errors"
)

func NewDBC(d *descriptor.Database, f diagnostics.Flags, s map[int]state.StateFunc) DBCInterface {
	return &DBC{database: d, diagnosticFlags: f, stateFuncs: s}
}

type DBCInterface interface {
	GetActiveDiagnosticFlags(vin string, msg *kafka_grpc.GRPC_CANFrame) ([]common.CANSignal, bool, error)
	GetStateFunc(id int) (state.StateFunc, bool)
	GenerateCANSignals(vin string, msg *kafka_grpc.GRPC_CANFrame) ([]common.CANSignal, error)
	GenerateCANSignalsMap(vin string, msg *kafka_grpc.GRPC_CANFrame) (map[string]common.CANSignal, error)
	DecodeMessage(msg *kafka_grpc.GRPC_CANFrame) ([]descriptor.DecodedSignal, error)
	GetDBCHash() string
	SetDBCHash(string)
	GetDatabase() *descriptor.Database
}

type DBC struct {
	database        *descriptor.Database
	hash            string
	diagnosticFlags diagnostics.Flags
	stateFuncs      map[int]state.StateFunc
}

func (d *DBC) GetStateFunc(id int) (state.StateFunc, bool) {
	f, ok := d.stateFuncs[id]
	return f, ok
}

// GenerateCANSignals parses raw can bus message (encoded in base64) to ECU data
func (d *DBC) GenerateCANSignals(vin string, msg *kafka_grpc.GRPC_CANFrame) ([]common.CANSignal, error) {
	decodedSignals, err := d.DecodeMessage(msg)
	if err != nil {
		return nil, err
	}

	signals := make([]common.CANSignal, 0, len(decodedSignals))
	for _, signal := range decodedSignals {
		signals = append(signals, common.CANSignal{
			VIN:       vin,
			Timestamp: float64(msg.Epoch) * math.Pow(10, -6),
			ID:        int(msg.ID),
			Name:      signal.Signal.Name,
			Value:     signal.Value,
		})
	}

	return signals, nil
}

// GenerateCANSignalSets parses raw can bus message (encoded in base64) to ECU
func (d *DBC) GenerateCANSignalsMap(vin string, msg *kafka_grpc.GRPC_CANFrame) (map[string]common.CANSignal, error) {
	var signals map[string]common.CANSignal

	decodedSignals, err := d.DecodeMessage(msg)
	if err != nil {
		return signals, err
	}
	signals = make(map[string]common.CANSignal, len(decodedSignals))
	for _, signal := range decodedSignals {
		signals[signal.Signal.Name] = common.CANSignal{
			VIN:       vin,
			Timestamp: float64(msg.Epoch) * math.Pow(10, -6),
			ID:        int(msg.ID),
			Name:      signal.Signal.Name,
			Value:     signal.Value,
		}
	}

	return signals, nil
}

func (d *DBC) GetActiveDiagnosticFlags(vin string, msg *kafka_grpc.GRPC_CANFrame) ([]common.CANSignal, bool, error) {
	var flags []common.CANSignal

	diagnosticSignals, ok := d.diagnosticFlags[int(msg.ID)]
	if !ok {
		return flags, false, nil
	}

	decodedSignals, err := d.DecodeMessage(msg)
	if err != nil {
		return flags, false, err
	}

	flags = make([]common.CANSignal, 0)
	for _, signal := range decodedSignals {
		faultVal, ok := diagnosticSignals[signal.Signal.Name]

		if !ok {
			continue
		} else if signal.Value != faultVal {
			continue
		}

		flags = append(flags, common.CANSignal{
			VIN:       vin,
			Timestamp: float64(msg.Epoch) * math.Pow(10, -6),
			ID:        int(msg.ID),
			Name:      signal.Signal.Name,
			Value:     signal.Value,
		})
	}

	return flags, len(flags) != 0, nil
}

func (d *DBC) DecodeMessage(msg *kafka_grpc.GRPC_CANFrame) ([]descriptor.DecodedSignal, error) {
	var decodedSignals []descriptor.DecodedSignal

	if msg.ID >= 2048 || msg.ID < 0 {
		return decodedSignals, errors.Errorf("message id %v out of bounds", msg.ID)
	}
	message, ok := d.database.Message(uint32(msg.ID))
	if !ok {
		return decodedSignals, errors.Errorf(
			"message id %v not found in %s, version: %s",
			msg.ID, d.database.SourceFile, d.database.Version)
	}

	strippedData := strings.TrimRight(string(msg.Value), "=")
	data, err := base64.RawStdEncoding.DecodeString(strippedData)
	if err != nil {
		return decodedSignals, errors.WithStack(err)
	}

	p := can.Payload{Data: data}
	decodedSignals = message.Decode(&p)
	return decodedSignals, nil
}

func (d *DBC) GetDBCHash() string {
	return d.hash
}

func (d *DBC) GetDatabase() *descriptor.Database {
	return d.database
}

func (d *DBC) SetDBCHash(hash string) {
	d.hash = hash
}
