/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

// This file contains functionality for loading mfg definitions from `mfg.yml`
// files.

package mfg

import (
	"github.com/spf13/cast"

	"mynewt.apache.org/newt/newt/ycfg"
	"mynewt.apache.org/newt/util"
)

// Indicates that an element is located at the end of a flash area.
const OFFSET_END = -1

type DecodedTarget struct {
	Name          string
	Area          string
	Offset        int
	ExtraManifest map[string]interface{}
}

type DecodedRaw struct {
	Filename      string
	Area          string
	Offset        int
	ExtraFiles    []string
	ExtraManifest map[string]interface{}
}

type DecodedMmrRef struct {
	Area string
}

type DecodedMeta struct {
	Area     string
	Hash     bool
	FlashMap bool
	Mmrs     []DecodedMmrRef
}

type DecodedMfg struct {
	Targets []DecodedTarget
	Raws    []DecodedRaw
	Meta    *DecodedMeta

	// Only required if no targets present.
	Bsp string
}

func decodeOffsetStr(offsetStr string) (int, error) {
	if offsetStr == "end" {
		return OFFSET_END, nil
	}

	offsetInt, err := cast.ToIntE(offsetStr)
	if err != nil {
		return 0, util.FmtNewtError("invalid offset value: \"%s\"", offsetStr)
	}

	return offsetInt, nil
}

func decodeBool(kv map[string]interface{}, key string) (*bool, error) {
	var bp *bool

	val := kv[key]
	if val != nil {
		b, err := cast.ToBoolE(val)
		if err != nil {
			return nil, util.FmtNewtError(
				"invalid `%s` value \"%v\"; "+
					"value must be either \"true\" or \"false\"", key, val)
		}

		bp = &b
	}

	return bp, nil
}

func decodeBoolDflt(kv map[string]interface{}, key string,
	dflt bool) (bool, error) {

	bp, err := decodeBool(kv, key)
	if err != nil {
		return false, err
	}

	if bp == nil {
		return dflt, nil
	} else {
		return *bp, nil
	}
}

func decodeExtra(kv map[string]interface{},
	key string) (map[string]interface{}, error) {

	extra := kv[key]
	if extra == nil {
		return nil, nil
	}

	if _, ok := extra.(map[interface{}]interface{}); !ok {
		return nil, util.FmtNewtError(
			"invalid \"%s\" field: must be a map", key)
	}

	// If `extra` contains any maps, they were unmarshalled as
	// `map[interface{}]interface{}`.  These need to be converted to
	// `map[string]interface{}` to be JSON compatible.
	var fixMap func(m map[string]interface{})
	fixMap = func(m map[string]interface{}) {
		for k, v := range m {
			if _, ok := v.(map[interface{}]interface{}); ok {
				sm := cast.ToStringMap(v)
				fixMap(sm)

				m[k] = sm
			}
		}
	}

	extraMap := cast.ToStringMap(extra)
	fixMap(extraMap)

	return extraMap, nil
}

func decodeTarget(yamlTarget interface{}) (DecodedTarget, error) {
	dt := DecodedTarget{}

	kv, err := cast.ToStringMapE(yamlTarget)
	if err != nil {
		return dt, util.FmtNewtError(
			"mfg contains invalid `mfg.targets` map: %s", err.Error())
	}

	nameVal := kv["name"]
	if nameVal == nil {
		return dt, util.FmtNewtError(
			"mfg target entry missing required field \"name\"")
	}
	dt.Name = cast.ToString(nameVal)

	areaVal := kv["area"]
	if areaVal == nil {
		return dt, util.FmtNewtError(
			"target entry \"%s\" missing required field \"area\"", dt.Name)
	}
	dt.Area = cast.ToString(areaVal)

	offsetVal := kv["offset"]
	if offsetVal == nil {
		return dt, util.FmtNewtError(
			"target entry \"%s\" missing required field \"offset\"", dt.Name)
	}
	offsetStr := cast.ToString(offsetVal)
	offsetInt, err := decodeOffsetStr(offsetStr)
	if err != nil {
		return dt, util.FmtNewtError(
			"in target entry \"%s\": %s", dt.Name, err.Error())
	}
	dt.Offset = offsetInt

	extra, err := decodeExtra(kv, "extra_manifest")
	if err != nil {
		return dt, util.FmtNewtError(
			"in target entry %s: %s", dt.Name, err.Error())
	}
	dt.ExtraManifest = extra

	return dt, nil
}

func decodeRaw(yamlRaw interface{}, entryIdx int) (DecodedRaw, error) {
	dr := DecodedRaw{}

	kv, err := cast.ToStringMapE(yamlRaw)
	if err != nil {
		return dr, util.FmtNewtError(
			"mfg contains invalid `mfg.raw` map: %s", err.Error())
	}

	areaVal := kv["area"]
	if areaVal == nil {
		return dr, util.FmtNewtError(
			"raw entry missing required field \"area\"")
	}
	dr.Area = cast.ToString(areaVal)

	offsetVal := kv["offset"]
	if offsetVal == nil {
		return dr, util.FmtNewtError(
			"mfg raw entry missing required field \"offset\"")
	}
	offsetStr := cast.ToString(offsetVal)
	offsetInt, err := decodeOffsetStr(offsetStr)
	if err != nil {
		return dr, util.FmtNewtError(
			"in raw entry %d: %s", entryIdx, err.Error())
	}
	dr.Offset = offsetInt

	filenameVal := kv["name"]
	if filenameVal == nil {
		return dr, util.FmtNewtError(
			"mfg raw entry missing required field \"name\"")
	}
	dr.Filename = cast.ToString(filenameVal)

	if itf, ok := kv["extra_files"]; ok {
		extraFiles, err := cast.ToStringSliceE(itf)
		if err != nil {
			return dr, util.FmtNewtError(
				"mfg raw entry contains invalid \"extra_files\" field: "+
					"have=%T want=[]string", itf)
		}
		dr.ExtraFiles = extraFiles
	}

	extra, err := decodeExtra(kv, "extra_manifest")
	if err != nil {
		return dr, util.FmtNewtError(
			"in raw entry %d: %s", entryIdx, err.Error())
	}
	dr.ExtraManifest = extra

	return dr, nil
}

func decodeMmr(yamlMmr interface{}) (DecodedMmrRef, error) {
	dm := DecodedMmrRef{}

	kv, err := cast.ToStringMapE(yamlMmr)
	if err != nil {
		return dm, util.FmtNewtError(
			"mfg meta contains invalid `mmrs` sequence: %s", err.Error())
	}

	areaVal := kv["area"]
	if areaVal == nil {
		return dm, util.FmtNewtError(
			"mmr entry missing required field \"area\"")
	}
	dm.Area = cast.ToString(areaVal)

	return dm, nil
}

func decodeMmrs(yamlMmrs interface{}) ([]DecodedMmrRef, error) {
	yamlSlice, err := cast.ToSliceE(yamlMmrs)
	if err != nil {
		return nil, util.FmtNewtError(
			"mfg meta contains invalid `mmrs` sequence: %s", err.Error())
	}

	mmrs := []DecodedMmrRef{}
	for _, yamlMmr := range yamlSlice {
		mmr, err := decodeMmr(yamlMmr)
		if err != nil {
			return nil, err
		}
		mmrs = append(mmrs, mmr)
	}

	return mmrs, nil
}

func decodeMeta(
	kv map[string]interface{}) (DecodedMeta, error) {

	dm := DecodedMeta{}

	areaVal := kv["area"]
	if areaVal == nil {
		return dm, util.FmtNewtError(
			"meta map missing required field \"area\"")
	}
	dm.Area = cast.ToString(areaVal)

	hash, err := decodeBoolDflt(kv, "hash", false)
	if err != nil {
		return dm, err
	}
	dm.Hash = hash

	fm, err := decodeBoolDflt(kv, "flash_map", false)
	if err != nil {
		return dm, err
	}
	dm.FlashMap = fm

	yamlMmrs := kv["mmrs"]
	if yamlMmrs != nil {
		mmrs, err := decodeMmrs(yamlMmrs)
		if err != nil {
			return dm, err
		}
		dm.Mmrs = mmrs
	}

	return dm, nil
}

func decodeMfg(yc ycfg.YCfg) (DecodedMfg, error) {
	dm := DecodedMfg{}

	yamlTargets, err := yc.GetValSlice("mfg.targets", nil)
	util.OneTimeWarningError(err)

	if yamlTargets != nil {
		for _, yamlTarget := range yamlTargets {
			t, err := decodeTarget(yamlTarget)
			if err != nil {
				return dm, err
			}

			dm.Targets = append(dm.Targets, t)
		}
	}

	dm.Bsp, err = yc.GetValString("mfg.bsp", nil)
	util.OneTimeWarningError(err)

	if len(dm.Targets) == 0 && dm.Bsp == "" {
		return dm, util.FmtNewtError(
			"\"mfg.bsp\" field required for mfg images without any targets")
	}

	itf, err := yc.GetValSlice("mfg.raw", nil)
	util.OneTimeWarningError(err)

	slice := cast.ToSlice(itf)
	if slice != nil {
		for i, yamlRaw := range slice {
			raw, err := decodeRaw(yamlRaw, i)
			if err != nil {
				return dm, err
			}

			dm.Raws = append(dm.Raws, raw)
		}
	}

	yamlMeta, err := yc.GetValStringMap("mfg.meta", nil)
	util.OneTimeWarningError(err)

	if yamlMeta != nil {
		meta, err := decodeMeta(yamlMeta)
		if err != nil {
			return dm, err
		}
		dm.Meta = &meta
	}

	return dm, nil
}
