package cn.ibizlab.codegen.model;


import cn.ibizlab.codegen.utils.StringAdvUtils;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.annotation.JSONField;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import cn.ibizlab.codegen.utils.Inflector;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.experimental.Accessors;
import net.ibizsys.model.dataentity.IPSDataEntity;
import net.ibizsys.model.dataentity.defield.IPSDEField;
import org.springframework.util.StringUtils;

import java.io.File;
import java.math.BigDecimal;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;

@Getter
@Setter
@NoArgsConstructor
@Accessors(chain = true)
@JsonInclude(JsonInclude.Include.NON_EMPTY)
@JsonIgnoreProperties(ignoreUnknown = true)
public class PojoSchema {




    @JSONField(ordinal = 1)
    private String id;

    @JSONField(ordinal = 2)
    private String type;


    @JSONField(serialize = false)
    @JsonIgnore
    private String name;

    @JSONField(serialize = false)
    @JsonIgnore
    public String getName()
    {
        if(name==null)
            name=this.getOptions().getName();
        return name;
    }

    public PojoSchema setName(String name)
    {
        if(!StringUtils.isEmpty(name))
        {
            this.name=name;
            if(options==null)
                options=new PojoOption();
            options.setName(name);
        }
        return this;
    }

    @JSONField(serialize = false)
    @JsonIgnore
    public String getCodeName()
    {
        return this.getOptions().getStringValue("code_name",this.getName());
    }

    @JSONField(serialize = false)
    @JsonIgnore
    public String getSystem()
    {
        return this.getOptions().getSystem();
    }

    @JSONField(serialize = false)
    @JsonIgnore
    public String getDefaultDataSoruce()
    {
        return this.getOptions().getDefaultDataSoruce();
    }

    @JSONField(serialize = false)
    @JsonIgnore
    public String getDefaultTableName()
    {
        return this.getOptions().getTableName();
    }

    @JSONField(serialize = false)
    @JsonIgnore
    public String getModule()
    {
        return this.getOptions().getModule();
    }

    @JSONField(serialize = false)
    @JsonIgnore
    private Object node;


    public String getStorageMode(){
        return this.getOptions().getStorageMode();
    }

    @JSONField(serialize = false)
    @JsonIgnore
    public IPSDataEntity getDataEntity()
    {
        if(node != null)
        {
            if(node instanceof IPSDataEntity)
                return (IPSDataEntity)node;
            else if(node instanceof IPSDEField && owner!=null)
                return owner.getDataEntity();
        }
        return null;
    }

    @JSONField(serialize = false)
    @JsonIgnore
    public IPSDEField getField()
    {
        if(node != null)
        {
            if(node instanceof IPSDEField)
                return (IPSDEField)node;
        }
        return null;
    }

    @JSONField(ordinal = 3)
    private String title;

    @JSONField(ordinal = 4)
    private String description;

    @JSONField(ordinal = 5)
    private Integer propertyOrder;

    @JSONField(ordinal = 6)
    private PojoOption options;

    public PojoOption getOptions()
    {
        if(options==null)
            options=new PojoOption();
        return options;
    }

    public PojoSchema setOptions(PojoOption options)
    {
        if(options==null)
            return this;

        if(this.options!=null)
            options.putAll(this.options);
        this.options=options;
        return this;
    }

    @JSONField(ordinal = 7)
    private String ref;

    @JSONField(serialize = false)
    @JsonIgnore
    public PojoSchema copy()
    {
        return JSONObject.parseObject(JSON.toJSONString(this),PojoSchema.class);
    }

    @JSONField(serialize = false)
    @JsonIgnore
    private boolean built = false;

    @JSONField(serialize = false)
    @JsonIgnore
    public PojoSchema build()
    {
        if((!built)&& Type.object.getCode().equalsIgnoreCase(this.getType()))
        {
            if(properties!=null)
            {
                properties.values().forEach(prop->{
                    PojoSchema item=null;
                    if(Type.object.getCode().equalsIgnoreCase(prop.getType())&&(!StringUtils.isEmpty(prop.getRef())))
                        item=prop;
                    else if(Type.array.getCode().equalsIgnoreCase(prop.getType())
                            &&prop.getItems()!=null&& Type.object.getCode().equalsIgnoreCase(prop.getItems().getType())
                            &&(!StringUtils.isEmpty(prop.getItems().getRef())))
                        item=prop.getItems();
                    else
                        return;
                });
            }
            built=true;
        }
        return this;
    }

    /// object

    @JSONField(ordinal = 11)
    private Map<String,PojoSchema> properties;

    public PojoSchema setProperties(Map<String,PojoSchema> properties)
    {
        if(properties!=null)
        {
            properties.keySet().forEach(key->{
                properties.get(key).setName(key).setOwner(this);
            });
            this.properties=properties;
        }
        return this;
    }

    public Map<String,PojoSchema> getProperties()
    {
        if(!Type.object.getCode().equals(this.type))
        {
            properties=null;
            return properties;
        }
        if(properties==null)
            properties = new LinkedHashMap<>();
        return properties;
    }

    public PojoSchema addProperty(String name, PojoSchema property)
    {
        if(StringUtils.isEmpty(name))
            return this;
        Map<String,PojoSchema> props = this.getProperties();
        if(props!=null) {
            props.put(name, property.setName(name).setOwner(this));
        }
        return this;
    }


    @JsonIgnore
    @JSONField(serialize = false)
    private List<PojoSchema> references;
    public List<PojoSchema> getReferences()
    {
        if(references==null)
        {
            references=new ArrayList<>();
            getProperties().values().forEach(prop->{
                if(Type.object.getCode().equalsIgnoreCase(prop.getType())&&(!StringUtils.isEmpty(prop.getOptions().getRelationName()))) {
                    references.add(prop);
                }
            });
        }
        return references;
    }

    @JsonIgnore
    @JSONField(serialize = false)
    private Map<String,PojoSchema> referenceMap;

    @JsonIgnore
    @JSONField(serialize = false)
    public Map<String,PojoSchema> getReferenceMap()
    {
        if(referenceMap==null)
        {
            referenceMap=new LinkedHashMap<>();
            getProperties().keySet().forEach(key->{
                PojoSchema prop=getProperties().get(key);
                if(Type.object.getCode().equalsIgnoreCase(prop.getType())&&(!StringUtils.isEmpty(prop.getOptions().getRelationName()))) {
                    referenceMap.put(key, prop);
                    if(!key.equals(key.toLowerCase()))
                        referenceMap.put(key.toLowerCase(),prop);
                    referenceMap.put(prop.getOptions().getRelationName(),prop);
                    if(!StringUtils.isEmpty(prop.getOptions().getEntityName())) {
                        if(!referenceMap.containsKey(prop.getOptions().getEntityName()))
                            referenceMap.put(prop.getOptions().getEntityName(), prop);
                        if(!referenceMap.containsKey(prop.getOptions().getEntityName().toLowerCase()))
                            referenceMap.put(prop.getOptions().getEntityName().toLowerCase(),prop);
                    }
                    if(!StringUtils.isEmpty(prop.getOptions().getCodeName())) {
                        if(!referenceMap.containsKey(prop.getOptions().getCodeName()))
                            referenceMap.put(prop.getOptions().getCodeName(), prop);
                        if(!referenceMap.containsKey(prop.getOptions().getCodeName().toLowerCase()))
                            referenceMap.put(prop.getOptions().getCodeName().toLowerCase(), prop);
                    }
                    String pluralize= Inflector.getInstance().pluralize(StringAdvUtils.camelcase(StringUtils.isEmpty(prop.getOptions().getCodeName())?prop.getOptions().getEntityName():prop.getOptions().getCodeName()).toLowerCase());
                    if(!referenceMap.containsKey(pluralize))
                        referenceMap.put(pluralize, prop);
                }
            });
        }
        return referenceMap;
    }

    public PojoSchema getRefSchema(String tag)
    {
        if(getReferenceMap()!=null)
        {
            return this.getReferenceMap().get(tag);
        }
        return null;
    }

    public Map<String,PojoSchema> getRefProperties()
    {
        Map<String,PojoSchema> refProperties=new LinkedHashMap<>();
        if(this.getOwner()!=null&& Type.object.getCode().equalsIgnoreCase(this.getType())&&(!StringUtils.isEmpty(this.getOptions().getRelationName())))
        {
            this.getOwner().getProperties().entrySet().forEach(entry-> {
                String key=entry.getKey();
                if(key.equals(this.getName()))
                    return;
                PojoSchema prop = entry.getValue();
                if((!StringUtils.isEmpty(prop.getOptions().getRelationName()))&&prop.getOptions().getRelationName().equals(this.getOptions().getRelationName()))
                {
                    refProperties.put(key,prop);
                }
            });
        }
        return refProperties;
    }

    public Map<String,PojoSchema> getRefProperties(String tag)
    {
        Map<String,PojoSchema> refProperties=new LinkedHashMap<>();
        PojoSchema refSchema=this.getRefSchema(tag);
        if(refSchema!=null)
        {
            getProperties().entrySet().forEach(entry-> {
                String key=entry.getKey();
                if(key.equals(refSchema.getName()))
                    return;
                PojoSchema prop = entry.getValue();
                if((!StringUtils.isEmpty(prop.getOptions().getRelationName()))&&prop.getOptions().getRelationName().equals(refSchema.getOptions().getRelationName()))
                {
                    refProperties.put(key,prop);
                }
            });
        }
        return refProperties;
    }


    @JSONField(serialize = false)
    @JsonIgnore
    private PojoSchema owner;

    @JSONField(ordinal = 12)
    private Set<String> required;

    public Set<String> getRequired()
    {
        if(!Type.object.getCode().equals(this.type))
        {
            required=null;
            return required;
        }
        if(required==null&&properties!=null)
        {
            required=new LinkedHashSet<>();
            properties.values().forEach(prop->{
                if (!prop.getOptions().isNullable())
                    required.add(prop.getName());
            });
        }
        return required;
    }

    @JSONField(ordinal = 13)
    private Integer minProperties;

    @JSONField(ordinal = 14)
    private Integer maxProperties;


    @JsonIgnore
    @JSONField(serialize = false)
    private Map<String,PojoSchema> keyMap;

    @JsonIgnore
    @JSONField(serialize = false)
    private PojoSchema keyProperty;


    @JsonIgnore
    @JSONField(serialize = false)
    private Map<String,PojoSchema> unionKeys;

    public PojoSchema getKeyProperty() {
        if(keyProperty==null)
            getKeyMap();
        return keyProperty;
    }

    @JsonIgnore
    @JSONField(serialize = false)
    public synchronized Map<String,PojoSchema> getKeyMap()
    {
        if(Type.object.getCode().equals(this.type))
        {
            if(keyMap==null)
            {
                List<PojoSchema> keys=new ArrayList<>();
                keyMap=new LinkedHashMap<>();
                getProperties().values().forEach(sub->{
                    if(Type.array.getCode().equals(sub.getType())|| Type.object.getCode().equals(sub.getType()))
                        return;
                    if(sub.getOptions().isKeyField())
                        keyProperty=sub;
                    if(sub.getOptions().isKeyField()&&sub.getOptions().isPhysicalField())
                        keys.add(sub);
                });
                if(keys.isEmpty())
                {
                    getProperties().values().forEach(sub->{
                        if(Type.array.getCode().equals(sub.getType())|| Type.object.getCode().equals(sub.getType()))
                            return;
                        if(sub.getOptions().isUnionKeyField()&&sub.getOptions().isPhysicalField())
                            keys.add(sub);
                    });
                }
                if(keys.size()>0)
                    keys.sort( (o1, o2) -> o1.getOptions().getUnionKey().compareTo(o2.getOptions().getUnionKey()) );

                keys.forEach(sub->keyMap.put(sub.getOptions().getFieldName(),sub));
            }

        }
        return keyMap;
    }

    @JsonIgnore
    @JSONField(serialize = false)
    public synchronized Map<String,PojoSchema> getUnionKeys()
    {
        if(Type.object.getCode().equals(this.type)) {
            if (unionKeys == null) {
                List<PojoSchema> keys=new ArrayList<>();
                Map<String,PojoSchema> unions=new LinkedHashMap<>();

                getProperties().values().forEach(sub->{
                    if(Type.array.getCode().equals(sub.getType())|| Type.object.getCode().equals(sub.getType()))
                        return;
                    if(sub.getOptions().isUnionKeyField()&&sub.getOptions().isPhysicalField())
                        keys.add(sub);
                });
                if(keys.size()>0) {
                    keys.sort((o1, o2) -> o1.getOptions().getUnionKey().compareTo(o2.getOptions().getUnionKey()));
                    keys.forEach(sub -> unions.put(sub.getOptions().getFieldName(),sub));
                    unionKeys=unions;
                }
            }
        }
        return unionKeys;
    }


    ////  array

    @JSONField(ordinal = 21)
    private PojoSchema items;

    @JSONField(ordinal = 22)
    private Integer minItems;

    @JSONField(ordinal = 23)
    private Boolean uniqueItems;

    public void setUniqueItems(Boolean uniqueItems) {
        this.uniqueItems = uniqueItems;
    }

    ///  String

    @JSONField(ordinal = 33)
    private Integer minLength;

    @JSONField(ordinal = 34)
    private Integer maxLength;

    @JSONField(ordinal = 35)
    private String pattern;

    @JSONField(ordinal = 36)
    private String format;

    ///  integer number

    @JSONField(ordinal = 41)
    private BigDecimal minimum;

    @JSONField(ordinal = 42)
    private BigDecimal maximum;

    @JSONField(ordinal = 43)
    private Boolean exclusiveMinimum;

    @JSONField(ordinal = 44)
    private Boolean exclusiveMaximum;

    @JSONField(ordinal = 45)
    private Integer multipleOf;


    public PojoSchema writeTo(Path path)
    {
        return writeTo(this,path);
    }

    public static PojoSchema fromPath(Path path)
    {
        try {
            if(!Files.exists(path))
                throw new IllegalArgumentException("读取文件失败PojoSchema:"+path.toString());
            return JSON.parseObject(Files.readAllBytes(path),PojoSchema.class);

        } catch (Exception e) {
            throw new RuntimeException("解析文件失败PojoSchema:"+path.toString());
        }
    }

    public static PojoSchema writeTo(PojoSchema pojoSchema, Path path)
    {
        try {
            File dir=path.getParent().toFile();
            if(!dir.exists())
                dir.mkdirs();

            Files.write(path, JSON.toJSONBytes(pojoSchema));
        } catch (Exception e) {
            throw new RuntimeException("保存文件失败PojoSchema:"+path.toString());
        }
        return pojoSchema;
    }


    public static enum Type {
        string("string", "字符"),
        integer("integer", "整型"),
        number("number", "数值"),
        object("object", "对象"),
        array("array", "数组");
        public final String code;
        public final String name;

        private Type(String code, String name) {
            this.code = code;
            this.name = name;
        }

        public String getCode() {
            return code;
        }

        public String getName() {
            return name;
        }

        @Override
        public String toString() {
            return "Type{" +
                    "code='" + code + '\'' +
                    ", name='" + name + '\'' +
                    '}';
        }
    }




    @JSONField(serialize = false)
    @JsonIgnore
    private Map<String,POSchema> poSchemas;

    @JSONField(serialize = false)
    @JsonIgnore
    public POSchema getDefaultPOSchema()
    {
        return getPOSchema("default");
    }
    public PojoSchema addPOSchema(String name, POSchema poSchema)
    {
        if(poSchema!=null)
        {
            if(poSchemas==null)
                poSchemas=new LinkedHashMap<>();
            poSchemas.put(name,poSchema.build());
        }

        return this;
    }
    public POSchema getPOSchema(String name)
    {

        if(StringUtils.isEmpty(name)&&(!StringUtils.isEmpty(this.getDefaultDataSoruce())))
            name=this.getDefaultDataSoruce();
        if(StringUtils.isEmpty(name))
            name="mysql";

        if(poSchemas==null)
            poSchemas=new LinkedHashMap<>();
        if(poSchemas.containsKey(name))
        {
            return poSchemas.get(name);
        }
        String vendorProvider=POSchema.provider.get(name.toLowerCase());
        if((!StringUtils.isEmpty(vendorProvider))&&(!name.equalsIgnoreCase(vendorProvider))&&poSchemas.containsKey(vendorProvider))
            return poSchemas.get(vendorProvider);


        if("mongodb".equals(name))
        {
            POSchema documentPOSchema= TransUtils.PojoSchema2DocumentPO(this);
            if(documentPOSchema!=null) {
                poSchemas.put(name, documentPOSchema);
                return documentPOSchema;
            }
        }


        return null;
    }

}
