package tst

import (
	"fmt"
	"strings"

	. "git.eugeniocarvalho.dev/eugeniucarvalho/apicodegen/common" // . "git.eugeniocarvalho.dev/eugeniucarvalho/apicodegen/gen"
	TS "git.eugeniocarvalho.dev/eugeniucarvalho/gg/generators/typescript"
)

func GenSchemas(p *Project) error {
	var (
		attribName      string
		propName        string
		attribs         []TS.CodeInterface
		attrib          *TS.Group
		entityInfo      *EntityInfo
		arrayProperties = []*Propertie{}
	)

	appendBasePatchClass()

	for _, entity := range p.Schemas {

		attribs = []TS.CodeInterface{}
		arrayProperties = []*Propertie{}

		entityInfo = p.ResponseEntity(entity.ID)

		for _, meta := range entity.Properties {

			propName = strings.Title(meta.ID)
			meta.FillTags(p, propName)

			// Registra a relaao entre as entidades
			// if meta.Relation {
			// 	SR.Add(&Relation{
			// 		Source:     meta.GetType(),
			// 		Target:     entity.ID,
			// 		Attr:       strings.Replace(meta.Tags["bson"], ",omitempty", "", 1),
			// 		Collection: entity.Collection,
			// 	})
			// }
			if meta.Array {
				arrayProperties = append(arrayProperties, meta)
			}

			// Adiciona as tags caso sejam definidas
			if meta.Tags != nil {
				if name, ok := meta.Tags["json"]; ok {
					attribName = strings.Split(name, ",")[0]
					if attribName == "-" {
						attribName = meta.ID
					}
				}
			} else {
				attribName = meta.ID
			}

			if attribName == "id" {
				attribName = "_id"
			}

			meta.Name = attribName
			// Adiciona a crescricao como comentario

			attrib = &TS.Group{}

			if meta.Description != "" {
				// propertie.Comment(meta.Description)
				attrib.Comment(meta.Description)
			}

			if attribName != "" {
				optional := "?"
				// if getCustom(meta.Custom, "ts.optional").(bool) {
				// if !meta.Required {
				// 	options = "?"
				// }
				// attrib.Add(TS.Public().Id(attribName).Op(":").Id(TS.ConvType(meta.Type)))
				attrib.Add(TS.Id(attribName).Id(optional).Op(":").Id(TS.ConvType(meta.Type)))
				if meta.Array {
					attrib.Add(TS.Index())
				}
				attrib.Endl()
				attribs = append(attribs, attrib)
			}
		} // Fim do loop dos atributos

		// attribs = append(attribs, TS.Constructor(TS.Raw("opts: any")).Block(TS.Raw("opts && Object.assign(this,opts);")))
		unshift := []TS.CodeInterface{TS.Id(`constructor(entity?:any){ if(entity){Object.assign(this, entity);}}`)}

		attribs = append(unshift, attribs...)

		module.Line().Export().Class().Id(strings.Title(entity.ID)).Block(
			// module.Line().Export().Interface().Id(strings.Title(entity.ID)).Block(
			attribs...,
		).Line()

		if !entityInfo.IsGeneric {
			createPathClass(entity, arrayProperties)
		}

	} // Fim do for de schema
	return nil
}

func createPathClass(entity *Entity, arrayProperties []*Propertie) {

	module.
		Line().
		Export().
		Class().
		Id(strings.Title(entity.ID + "Patch")).
		Extends().
		Id("BasePatch").
		BlockFun(func(g *TS.Group) {
			createClassProperties(g, entity, arrayProperties)

			createSetInitializeMethod(g, entity, arrayProperties)

			// createPatchMethod(g, entity, arrayProperties)

			createAddAndRemoveMethods(g, entity, arrayProperties)

			// g.Raw(`
			// protected emit(timeout = 0) {
			// 	super.emit(this.Patchs(), timeout);
			// }
			// `)
		}).
		Line()
}

func createClassProperties(g *TS.Group, entity *Entity, arrayProperties []*Propertie) {
	for _, meta := range arrayProperties {
		g.Line().Add(
			TS.Raw(fmt.Sprintf("%s = new Map<string, { item: %s, action: string, type: string }>();", meta.ID, strings.Title(entity.ID))),
		)
	}
}
func createSetInitializeMethod(g *TS.Group, entity *Entity, arrayProperties []*Propertie) {
	destructuring := []string{}

	for _, meta := range arrayProperties {
		destructuring = append(destructuring, fmt.Sprintf("%s=[]", meta.ID))
	}

	destructuringString := strings.Join(destructuring, ",")

	g.Add(TS.
		Line().
		Line().
		// Do().

		Constructor(TS.Raw(fmt.Sprintf("{%s} : %s", destructuringString, entity.ID))).
		BlockFun(func(block *TS.Group) {
			// block.Add(TS.Raw(`this.instance = instance;`).Line())
			block.Add(TS.Raw(`super();`).Line())

			// .Raw(`const {`)

			for _, meta := range arrayProperties {
				// block.Add(TS.Raw(fmt.Sprintf("if ($instance.%s) { this._set(this.%s, $instance.%s, '',-1);}", meta.ID, meta.ID, meta.ID)).Line())
				block.Add(TS.Raw(fmt.Sprintf("if (%s && %s.length) { this._set(this.%s, %s, '',-1);}", meta.ID, meta.ID, meta.ID, meta.ID)).Line())
			}

		}),
	)
}

func createPatchMethod(g *TS.Group, entity *Entity, arrayProperties []*Propertie) {

	g.Add(TS.Line().
		Raw(`Patchs()`).
		BlockFun(func(block *TS.Group) {

			block.Add(TS.Raw(`const patchs = {`))

			for _, meta := range arrayProperties {
				block.Add(TS.
					Line().
					Raw(fmt.Sprintf("Add%s : [],", strings.Title(meta.ID))).
					Line().
					Raw(fmt.Sprintf("Remove%s : [],", strings.Title(meta.ID))).
					Line().
					Raw(fmt.Sprintf("Replace%s : [],", strings.Title(meta.ID))),
				)
			}

			block.
				Line().
				Raw(`};`).
				Line().
				Raw(`return patchs;`).
				Line()
		}),
	)

}

func createAddAndRemoveMethods(g *TS.Group, entity *Entity, arrayProperties []*Propertie) {
	const timeout = 50

	for _, meta := range arrayProperties {
		g.Add(TS.
			Line().
			Id(fmt.Sprintf("Add%s", strings.Title(meta.ID))).
			Params(TS.Id("itens"), TS.Id(fmt.Sprintf("timeout = %d", timeout))).
			// Params(TS.Id("itens"), TS.Id("timeout")).
			Block(
				TS.Raw(fmt.Sprintf("this._set(this.%s, itens, 'add', timeout);return this;", meta.ID)),
			).
			Line().
			Id(fmt.Sprintf("Remove%s", strings.Title(meta.ID))).
			Params(TS.Id("itens"), TS.Id(fmt.Sprintf("timeout = %d", timeout))).
			// Params(TS.Id("itens"), TS.Id("timeout")).
			Block(
				TS.Raw(fmt.Sprintf("this._set(this.%s, itens, 'remove', timeout);return this;", meta.ID)),
			).
			Line().
			Id(fmt.Sprintf("Replace%s", strings.Title(meta.ID))).
			Params(TS.Id("itens"), TS.Id(fmt.Sprintf("timeout = %d", timeout))).
			Block(
				TS.Raw(fmt.Sprintf("this._set(this.%s, itens, 'replace', timeout);return this;", meta.ID)),
			),
		)
	}
}

func appendBasePatchClass() {
	module.Line().Raw(`
	export class BasePatch {
		protected $set = {};
		protected $subscription: any;
		public locked = false;
		public change$ = new EventEmitter<any>();
		public $transformPatch = (patch) => patch;

		Set(options, timeout = 0) {
			this.$set = {...this.$set, ...options};
			
			if (timeout >= 0){
				this.emit(this.patch(), timeout);
			}
		}
		
		protected emit(patch:any , timeout = 0) {
			if (this.$subscription) { clearTimeout(this.$subscription); }
			this.locked = true;
			this.$subscription = setTimeout(() => this.change$.emit(patch), timeout);
		}

		protected __set(list, add, rem, replace) {
			list.forEach(element => {
				const {item} = element;
				switch (element.action) {
					case 'add': add.push(item);	break;
					case 'replace': replace.push(item);	break;
					case 'remove': if (element.type !== 'new') { rem.push(item._id || item); }
				}
			});
		}
		
		public from(patches, timeout = 50){
			const {$set = {}} = patches;
			this.Set($set, timeout);
			
			Object.keys(patches).map(prop => {
				if (prop.startsWith('$')) { return;}
				if (typeof this[prop] === 'function') {
					this[prop](patches[prop], timeout);
				}
			});
		}

		protected _set(_map: Map<string, any>, itens, action, timeout = 50) {
			(itens || []).map(item => {
		
				const id = (item._id || item);
				const has = _map.has(id)
					, el = _map.get(id);
		
				switch (action) {
					case 'replace': 
					_map.set(id, {
						item,
						action: 'replace',
						type: 'new',
					});
					break;
					case 'add':
					if (!has) {
						_map.set(id, {
						item,
						action: 'add',
						type: 'new',
						});
					} else if (el.action === 'remove') {
						el.action = 'add';
					}
					break;
					case 'remove':
					if (has) {
						el.action = 'remove';
						break;
					}
					// tslint:disable-next-line:no-switch-case-fall-through
					default:
					_map.set(id, {
						item,
						action,
						type: '',
					});
				}
			});

			if (timeout >= 0){
				this.emit(this.patch(), timeout);
			}

		}

	    patch() {
			const $this = this;
			const patchs = {};

			if (Object.getOwnPropertyNames(this.$set).length) {
				patchs['$set'] = this.$set;
			}

			Object.keys($this).map(prop => {
				if (prop.startsWith('$')) {
					return;
				}
				const adds = [],replace = [],remove = [];
				const captalized = prop[0].toUpperCase() + prop.slice(1);
				
				if ($this[prop].size) {

					this.__set($this[prop], adds, remove, replace);
					
					if (adds.length){ patchs[`).Raw("`Add${captalized}`").Raw(`] = adds; }
					if (replace.length){ patchs[`).Raw("`Replace${captalized}`").Raw(`] = replace; }
					if (remove.length){ patchs[`).Raw("`Remove${captalized}`").Raw(`] = remove; }
				}
			});
			
			return this.$transformPatch(patchs);
		}
	}
	`)
}
