/*
shottower
Copyright (C) 2022 RÃ©my Boulanouar

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero 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 Affero General Public License for more details.

You should have received a copy of the GNU Affero General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/
/*
 * Shottower
 *
 * Shottower is the open source version of Shotstack which is a video, image and audio editing service that allows for the automated generation of videos, images and audio using JSON and a RESTful API.  You arrange and configure an edit and POST it to the API which will render your media and provide a file  location when complete.  For more details visit [shottower](https://github.com/DblK/shottower) or checkout our [getting started](https://shotstack.io/docs/guide/) documentation.  There are two main API's, one for editing and generating assets (Edit API) and one for managing hosted assets (Serve API).  The Edit API base URL is: <b>http://0.0.0.0:4000/{version}</b>  The Serve API base URL is: <b>http://0.0.0.0:4000/serve/{version}</b>
 *
 * API version: stage
 * Generated by: OpenAPI Generator (https://openapi-generator.tech)
 */

package openapi

import (
	"encoding/json"
	"fmt"

	"github.com/spf13/cast"
	"golang.org/x/exp/slices"
)

// Clip - A clip is a container for a specific type of asset, i.e. a title, image, video, audio or html. You use a Clip to define when an asset will display on the timeline, how long it will play for and transitions, filters and effects to apply to it.
type Clip struct {
	Asset interface{} `json:"asset"`

	// The s{tart position of the Clip on the timeline, in seconds.
	Start float32 `json:"start"`

	// The length, in seconds, the Clip should play for.
	Length float32 `json:"length"`

	// Set how the asset should be scaled to fit the viewport using one of the following options:    <ul>     <li>`cover` - stretch the asset to fill the viewport without maintaining the aspect ratio.</li>     <li>`contain` - fit the entire asset within the viewport while maintaining the original aspect ratio.</li>     <li>`crop` - scale the asset to fill the viewport while maintaining the aspect ratio. The asset will be cropped if it exceeds the bounds of the viewport.</li>     <li>`none` - preserves the original asset dimensions and does not apply any scaling.</li>   </ul>
	Fit string `json:"fit,omitempty"`

	// Scale the asset to a fraction of the viewport size - i.e. setting the scale to 0.5 will scale asset to half the size of the viewport. This is useful for picture-in-picture video and  scaling images such as logos and watermarks.
	Scale *float32 `json:"scale,omitempty"`

	// Place the asset in one of nine predefined positions of the viewport. This is most effective for when the asset is scaled and you want to position the element to a specific position. <ul>   <li>`top` - top (center)</li>   <li>`topRight` - top right</li>   <li>`right` - right (center)</li>   <li>`bottomRight` - bottom right</li>   <li>`bottom` - bottom (center)</li>   <li>`bottomLeft` - bottom left</li>   <li>`left` - left (center)</li>   <li>`topLeft` - top left</li>   <li>`center` - center</li> </ul>
	Position string `json:"position,omitempty"`

	Offset *Offset `json:"offset,omitempty"`

	Transition *Transition `json:"transition,omitempty"`

	// A motion effect to apply to the Clip. <ul>   <li>`zoomIn` - slow zoom in</li>   <li>`zoomOut` - slow zoom out</li>   <li>`slideLeft` - slow slide (pan) left</li>   <li>`slideRight` - slow slide (pan) right</li>   <li>`slideUp` - slow slide (pan) up</li>   <li>`slideDown` - slow slide (pan) down</li> </ul>
	Effect string `json:"effect,omitempty"`

	// A filter effect to apply to the Clip. <ul>   <li>`boost` - boost contrast and saturation</li>   <li>`contrast` - increase contrast</li>   <li>`darken` - darken the scene</li>   <li>`greyscale` - remove color</li>   <li>`lighten` - lighten the scene</li>   <li>`muted` - reduce saturation and contrast</li>   <li>`invert` - invert colors</li> </ul>
	Filter string `json:"filter,omitempty"`

	// Sets the opacity of the Clip where 1 is opaque and 0 is transparent.
	Opacity float32 `json:"opacity,omitempty"`

	Transform *Transformation `json:"transform,omitempty"`
}

func NewClip(data map[string]interface{}, asset interface{}) *Clip {
	clip := &Clip{
		Asset: asset,
	}

	if data["start"] != nil {
		clip.Start = cast.ToFloat32(data["start"].(float64))
	}
	if data["length"] != nil {
		clip.Length = cast.ToFloat32(data["length"].(float64))
	}
	if data["fit"] != nil {
		clip.Fit = data["fit"].(string)
	}
	if data["scale"] != nil {
		scale := cast.ToFloat32(data["scale"].(float64))
		clip.Scale = &scale
	}
	if data["position"] != nil {
		clip.Position = data["position"].(string)
	}
	if data["offset"] != nil {
		clip.Offset = NewOffset(data["offset"].(map[string]interface{}))
	}
	if data["transition"] != nil {
		clip.Transition = NewTransition(data["transition"].(map[string]interface{}))
	}
	if data["effect"] != nil {
		clip.Effect = data["effect"].(string)
	}
	if data["filter"] != nil {
		clip.Filter = data["filter"].(string)
	}
	if data["opacity"] != nil {
		clip.Opacity = cast.ToFloat32(data["opacity"].(float64))
	}
	if data["transform"] != nil {
		clip.Transform = NewTransformation(data["transform"].(map[string]interface{}))
	}

	return clip
}

func (s *Clip) UnmarshalJSON(data []byte) error {
	var obj map[string]interface{}
	err := json.Unmarshal(data, &obj)
	if err != nil {
		return err
	}

	var typeAsset = obj["asset"].(map[string]interface{})["type"].(string)

	asset := NewAsset(typeAsset, obj["asset"].(map[string]interface{}))

	*s = *NewClip(obj, asset)
	return nil
}

func (s *Clip) checkEnumValues() error {
	fitValues := []string{"cover", "contain", "crop", "none"}
	if s.Fit != "" && !slices.Contains(fitValues, s.Fit) {
		return &EnumError{Schema: "Clip", Field: "Fit", Value: s.Fit}
	}

	positionValues := []string{"top", "topRight", "right", "bottomRight", "bottom", "bottomLeft", "left", "topLeft", "center"}
	if s.Position != "" && !slices.Contains(positionValues, s.Position) {
		return &EnumError{Schema: "Clip", Field: "Position", Value: s.Position}
	}

	effectValues := []string{"zoomIn", "zoomOut", "slideLeft", "slideRight", "slideUp", "slideDown"}
	if s.Effect != "" && !slices.Contains(effectValues, s.Effect) {
		return &EnumError{Schema: "Clip", Field: "Effect", Value: s.Effect}
	}

	filterValues := []string{"boost", "contrast", "darken", "greyscale", "lighten", "muted", "invert"}
	if s.Filter != "" && !slices.Contains(filterValues, s.Filter) {
		return &EnumError{Schema: "Clip", Field: "Filter", Value: s.Filter}
	}

	return nil
}

func (s *Clip) ToFFMPEG(FFMPEGCommand FFMPEGCommand, sourceClip int, trackNumber int, currentClip int) error {
	var effects []string
	var audioEffects []string

	var typeAsset = GetAssetType(s.Asset)
	switch typeAsset { // nolint:exhaustive
	case VideoAssetType:
		var currentAsset = s.Asset.(*VideoAsset)

		// v1. Burn
		if currentAsset.Subtitle != nil {
			effects = append(effects, FFMPEGCommand.ClipSubtitleBurn(sourceClip, trackNumber, currentClip, currentAsset.Subtitle.Index))
		}
		// v2. Trim
		if currentAsset.Trim != 0 {
			effects = append(effects, FFMPEGCommand.ClipTrim(sourceClip, trackNumber, currentClip, currentAsset.Trim, currentAsset.Trim+s.Length))

			// a1. Trim
			if currentAsset.Volume != 0 {
				audioEffects = append(audioEffects, FFMPEGCommand.ClipAudioTrim(sourceClip, trackNumber, currentClip, currentAsset.Trim, currentAsset.Trim+s.Length))
			}
		} else {
			effects = append(effects, FFMPEGCommand.ClipTrim(sourceClip, trackNumber, currentClip, 0, s.Length))
		}

		// a2. Volume + Timing
		if currentAsset.Volume != 0 {
			audioEffects = append(audioEffects, FFMPEGCommand.ClipAudioVolume(sourceClip, trackNumber, currentClip, currentAsset.Volume))
			audioEffects = append(audioEffects, FFMPEGCommand.ClipAudioDelay(sourceClip, trackNumber, currentClip, s.Start*1000))
		}

		// v3. Crop
		if currentAsset.Crop != nil {
			effects = append(effects, FFMPEGCommand.ClipCrop(sourceClip, trackNumber, currentClip, currentAsset.Crop))
			effects = append(effects, FFMPEGCommand.ClipFillerOverlay(sourceClip, trackNumber, currentClip, FFMPEGCommand.ClipCropOverlayPosition(currentAsset.Crop)))
		}

	case ImageAssetType:
		var currentAsset = s.Asset.(*ImageAsset)
		effects = append(effects, FFMPEGCommand.ClipImage(sourceClip, trackNumber, currentClip, 0, s.Length))

		if currentAsset.Crop != nil {
			effects = append(effects, FFMPEGCommand.ClipCrop(sourceClip, trackNumber, currentClip, currentAsset.Crop))
			effects = append(effects, FFMPEGCommand.ClipFillerOverlay(sourceClip, trackNumber, currentClip, FFMPEGCommand.ClipCropOverlayPosition(currentAsset.Crop)))
		} else {
			effects = append(effects, FFMPEGCommand.ClipFillerOverlay(sourceClip, trackNumber, currentClip, "topLeft"))
		}
	default:
		fmt.Println("Type not handled for converting to FFMPEG", typeAsset.String())
		return nil
	}

	// Resize clip to ensure concat will work
	if s.Scale != nil {
		effects = append(effects, FFMPEGCommand.ClipResize(sourceClip, trackNumber, currentClip, *s.Scale))
		effects = append(effects, FFMPEGCommand.ClipFillerOverlay(sourceClip, trackNumber, currentClip, s.Position))
	} else {
		effects = append(effects, FFMPEGCommand.ClipResize(sourceClip, trackNumber, currentClip, 1))
	}

	_ = FFMPEGCommand.AddClip(
		trackNumber,
		FFMPEGCommand.ClipMerge(sourceClip, trackNumber, currentClip, effects),
	)
	_ = FFMPEGCommand.AddAudioClip(
		trackNumber,
		FFMPEGCommand.ClipAudioMerge(sourceClip, trackNumber, currentClip, audioEffects),
	)

	return nil
}

// AssertClipRequired checks if the required fields are not zero-ed
func AssertClipRequired(obj *Clip) error {
	elements := map[string]interface{}{
		"asset":  obj.Asset,
		"start":  obj.Start,
		"length": obj.Length,
	}
	for name, el := range elements {
		// TODO: start is number and can start with zero
		if name != "start" {
			if isZero := IsZeroValue(el); isZero {
				return &RequiredError{Schema: "Clip", Field: name}
			}
		}
	}

	if err := obj.checkEnumValues(); err != nil {
		return err
	}

	if err := AssertAssetRequired(&obj.Asset); err != nil {
		return err
	}
	if err := AssertOffsetRequired(obj.Offset); err != nil {
		return err
	}
	if err := AssertTransitionRequired(obj.Transition); err != nil {
		return err
	}
	if err := AssertTransformationRequired(obj.Transform); err != nil {
		return err
	}
	return nil
}

// AssertRecurseClipRequired recursively checks if required fields are not zero-ed in a nested slice.
// Accepts only nested slice of Clip (e.g. [][]Clip), otherwise ErrTypeAssertionError is thrown.
func AssertRecurseClipRequired(objSlice interface{}) error {
	return AssertRecurseInterfaceRequired(objSlice, func(obj interface{}) error {
		aClip, ok := obj.(Clip)
		if !ok {
			return ErrTypeAssertionError
		}
		return AssertClipRequired(&aClip)
	})
}
