package cc.util.bossshop.config;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.Validate;
import org.bukkit.configuration.InvalidConfigurationException;
import org.bukkit.configuration.file.YamlConstructor;
import org.bukkit.configuration.file.YamlRepresenter;
import org.yaml.snakeyaml.DumperOptions;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.error.YAMLException;
import org.yaml.snakeyaml.representer.Representer;

import cc.util.bossshop.FBukkit;
import cc.util.bossshop.config.YamlNode.ChildType;

/**
 * 支持注释的Yaml文件配置管理器
 * @author 聪聪
 *
 */
public class CommentedYamlConfig extends CommentedConfiguration{

    protected static final String BLANK_CONFIG="{}\n";
    protected final static Pattern CFG_NODE=Pattern.compile("^([ ]*)(?!- | |#)(.*?)(?=:):( .*)?$");
    protected final static Pattern CFG_VALUE=Pattern.compile("^([ ]*)- (.*?)$");
    protected final static Pattern COMMENT=Pattern.compile("^([ ]*)# ?(.*?)$");
    protected final Yaml yaml;
    protected final DumperOptions dumpOptions;
    
    /**
     * 使用指定的消息前缀实例化配置管理器
     * @param pLogPrefix 错误消息前缀
     */
    public CommentedYamlConfig(){
        this.dumpOptions=new DumperOptions();
        this.dumpOptions.setIndent(2);
        this.dumpOptions.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);
        this.dumpOptions.setAllowUnicode(true);
        if(!this.dumpOptions.isAllowUnicode()){
            Class<DumperOptions> clazz=DumperOptions.class;
            try{
                Field field=clazz.getDeclaredField("allowUnicode");
                field.setAccessible(true);
                field.setBoolean(dumpOptions,true);
            }catch(Exception exp){
                this.log("错误,无法设置文件存储为unicode编码",exp);
            }
        }
        Representer yamlRepresenter=new YamlRepresenter();
        yamlRepresenter.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);
        this.yaml=new Yaml(new YamlConstructor(),yamlRepresenter,dumpOptions);
    }

    /**
     * 在发生异常时用于输出调试信息,或输出警告信息到控制台
     * @param level 错误等级
     * @param msg 错误的提示消息
     * @param thrown 要抛出的异常
     */
    private void log(String msg,Throwable thrown){
        FBukkit.severe(msg,thrown);
    }

    @Override
    public void addDefault(String path,Object value){
        if(this.get(path)==null) this.set(path,value);
        super.addDefault(path,value);
    }

    /**
     * 将从字符串生成的配置节点数据格式化复制到本实例中
     * @param input 格式化后的数据
     * @param section 本实例,用于递归
     */
    protected void convertMapsToSections(Map<?,?> input,CommentedSection section){
        if(input==null) return;
        for(Map.Entry<?,?> entry : input.entrySet()){
            String key=entry.getKey().toString();
            Object value=entry.getValue();
            if((value instanceof Map))
                convertMapsToSections((Map<?,?>)value,section.createSection(key));
            else section.set(key,value);
        }
    }

    /**
     * 保存当前配置内存中的数据为字符串
     */
    public String saveToString(){
        String dump=this.yaml.dump(this.getValues(false));
        if(dump.equals(BLANK_CONFIG)) return "";
        else return this.injectComments(dump);
    }

    /**
     * 保存指定的数据到指定的文件,如果文件不存在将会自动创建
     * <p>保存数据过程中的任何错误都会被记录到控制台然后忽视</p>
     * @param pFile 指定的文件
     * @return 是否保存成功
     */
    public boolean saveToFile(File pFile){
        Validate.notNull(pFile,"配置文件不能为null");
        FileOutputStream output=null;
        try{
            this.tryCreateFile(pFile);
            output=new FileOutputStream(pFile,false);
            output.write(this.saveToString().getBytes("UTF-8"));
        }catch(FileNotFoundException ex){
            this.log("未找到文件["+pFile+"]",ex);
            return false;
        }catch(IOException ex){
            this.log("无法保存文件["+pFile+"]",ex);
            return false;
        }finally{
            if(output!=null) try{
                output.close();
            }catch(IOException exp){}
        }
        return true;
    }

    /**
     * 从字符串中载入配置
     */
    public void loadFromString(String pContents) throws InvalidConfigurationException{
        Validate.notNull(pContents,"字符串不能为null");
        Map<?,?> input=null;
        try{
            input=(Map<?,?>)this.yaml.load(pContents);
        }catch(YAMLException e){
            throw new InvalidConfigurationException(e);
        }catch(ClassCastException e){
            throw new InvalidConfigurationException("配置文件顶级节点不是Map");
        }
        this.clear();
        this.convertMapsToSections(input,this);
        this.extractComments(pContents);
    }

    /**
     * 从给定的文件路径中载入数据
     * <p>
     * 如果文件不存在将会自动创建<br />
     * 载入配置文件过程中的任何错误都会被记录到控制台然后忽视<br />
     * 如果载入失败,配置管理器内容将不变<br />
     * 编码默认使用 UTF-8
     * <p>
     *
     *@return 是否载入成功
     * @param pFilename 输入的文件路径
     * @throws NullPointerException 如果文件名为空
     */
    public boolean loadFromFile(String pFilename){
        Validate.notNull(pFilename,"文件名不能为null");
        return this.loadFromFile(new File(pFilename));
    }

    /**
     * 从给定的文件中载入数据
     * <p>
     * 如果文件不存在将会自动创建<br />
     * 载入配置文件过程中的任何错误都会被记录到控制台然后忽视<br />
     * 如果载入失败,配置管理器内容将不变<br />
     * 编码默认使用 UTF-8
     * <p>
     * 
     * @param pFile 输入文件
     * @return 是否成功加载
     * @throws IllegalArgumentException 如果文件为空
     */
    public boolean loadFromFile(File pFile){
        Validate.notNull(pFile,"文件不能为null");
        FileInputStream input=null;
        try{
            byte[] contents=new byte[(int)pFile.length()];
            if(!pFile.isFile()) this.tryCreateFile(pFile);
            input=new FileInputStream(pFile);
            input.read(contents);
            this.loadFromString(new String(contents,"UTF-8"));
        }catch(FileNotFoundException ex){
            this.log("无法找到文件["+pFile+"]",ex);
            return false;
        }catch(IOException ex){
            this.log("无法加载文件["+pFile+"]",ex);
            return false;
        }catch(InvalidConfigurationException ex){
            this.log("无法加载文件["+pFile+"],配置文件格式错误",ex);
            return false;
        }finally{
            if(input!=null) try{
                input.close();
            }catch(IOException exp){}
        }
        return true;
    }

    /**
     * 从给定的流中载入数据,流不会自动关闭
     * <p>
     * 如果文件不存在将会自动创建<br />
     * 载入配置文件过程中的任何错误都会被记录到控制台然后忽视<br />
     * 如果载入失败,配置管理器内容将不变<br />
     * 编码默认使用 UTF-8
     * <p>
     *
     * @param stream 输入的数据流
     * @return 是否载入成功
     * @throws IllegalArgumentException 如果输入流为空
     */
    public boolean loadFromStream(InputStream stream){
        Validate.notNull(stream,"输入流不能为空");
        try{
            byte[] contents=new byte[stream.available()];
            this.loadFromString(new String(contents,"UTF-8"));
        }catch(IOException ex){
            this.log("无法从输入流加载配置",ex);
            return false;
        }catch(InvalidConfigurationException ex){
            this.log("无法从输入流加载配置",ex);
            return false;
        }
        return true;
    }

    /**
     * 创建一个新文件<br/>
     * 如果文件已经存在,将什么都不干
     * @param pFile 文件
     * @throws IOException 创建文件时发生错误
     */
    protected void tryCreateFile(File pFile) throws IOException{
        Validate.notNull(pFile,"配置文件不能为null");
        if(pFile.exists()&&pFile.isFile()) return;
        if(!pFile.getParentFile().isDirectory()) pFile.getParentFile().mkdirs();
        pFile.createNewFile();
    }

    /**
     * 设置Yaml文件单极的空格缩进个数
     * @param pIndent 缩进
     */
    public void setIndent(int pIndent){
        this.dumpOptions.setIndent(pIndent);
    }

    /**
     * 获取Yaml文件单极的空格缩进个数
     * @param pIndent 缩进
     */
    public int getIndent(){
        return this.dumpOptions.getIndent();
    }

    /**
     * 清空配置管理器
     */
    public void clear(){
        this.map.clear();
        if(this.mDefaults!=null)
            this.mDefaults.map.clear();
    }

    /**从字符串中导出注释*/
    protected void extractComments(String pContent){
        if(pContent==null) return;
        pContent=pContent.replace("\\t","    ");
        ArrayList<String> comments=new ArrayList<>();
        YamlNode parent=new YamlNode("",-1,ChildType.NODE);
        YamlNode now=new YamlNode("root",0,ChildType.STRING);
        now.setParent(parent);
        String[] allLines=pContent.split("[\\r]?\n");
        Matcher matcher=null;
        char tWrapper='\'';
        int tIndent=this.getIndent();
        for(int i=0;i<allLines.length;i++){
            String sLine=allLines[i];
            if(StringUtils.isBlank(sLine)) continue;
            if((matcher=CommentedYamlConfig.CFG_NODE.matcher(sLine)).find()){ //模糊匹配到这是一个节点
                YamlNode tNode=LineAnalyse.analyse(sLine,tWrapper,tIndent);
                if(tNode==null) return ; //不是节点,,正则表达式同时约束了不是注释或者值,所以错误
                if(tNode.getDeep()==parent.getDeep()+1){       //当前父节点的的直接子节点
                    if(!parent.setChildType(ChildType.NODE)) return;
                }else if(tNode.getDeep()==parent.getDeep()+2){ //当前父节点的的直接孙子节点
                    if(!now.setChildType(ChildType.NODE)) return; //错误.挂了值的节点不能接配置节点
                    parent=now;
                }else if(tNode.getDeep()<parent.getDeep()+1){  //上级节点的子节点
                    do{
                        if((parent=parent.getParent())==null) return;    //错误,
                    }while(tNode.getDeep()<parent.getDeep()+1);
                }else return;   //错误
                tNode.setParent(parent);
                now=tNode;
                //设置注释,以及判断节点是否闭合
                if(!comments.isEmpty()){
                    this.setComments(tNode.getPath(),comments);
                    comments=new ArrayList<>();
                }
                while(!tNode.mClosed){
                    if(i>=allLines.length-1) return;  //读取完毕
                    if(LineAnalyse.hasCloseMark(allLines[++i],tWrapper)) break;
                }
                continue;
            }
            if((matcher=CommentedYamlConfig.CFG_VALUE.matcher(sLine)).find()){//值节点
                if(!now.setChildType(ChildType.ARRAY)) return ;
                int blackCount=matcher.group(1).length();
                if(blackCount!=now.getDeep()*tIndent) return ;  //错误,不在同一级
                boolean tClose=LineAnalyse.isCloseLine(matcher.group(2),tWrapper);
                while(!tClose){
                    if(i>=allLines.length-1) return;  //读取完毕
                    if(LineAnalyse.hasCloseMark(allLines[++i],tWrapper)) break;
                }
                if(!comments.isEmpty()) comments.clear();   //防止值注释混合到下个节点
                continue;
            }
            if((matcher=CommentedYamlConfig.COMMENT.matcher(sLine)).find()){//注释节点
                comments.add(matcher.group(2));
                continue;
            }
            return; //不认识的节点,或者处理的时候出错了
        }
    }
    
    /**将注释导入到字符串中*/
    protected String injectComments(String pContent){
        if(pContent==null) return "";
        ArrayList<String> contents=new ArrayList<>();
        YamlNode parent=new YamlNode("",-1,ChildType.NODE);
        YamlNode now=new YamlNode("root",0,ChildType.STRING);
        now.setParent(parent);
        String[] allLines=pContent.split("[\\r]?\n");
        Matcher matcher=null;
        char tWrapper='\'';
        int tIndent=this.getIndent();
        for(int i=0;i<allLines.length;i++){
            String sLine=allLines[i];
            if(StringUtils.isBlank(sLine)){
                contents.add("# ");
                continue;
            }
            if((matcher=CommentedYamlConfig.CFG_NODE.matcher(sLine)).find()){ //模糊匹配到这是一个节点
                YamlNode tNode=LineAnalyse.analyse(sLine,tWrapper,tIndent);
                if(tNode==null) return pContent; //不是节点,,正则表达式同时约束了不是注释或者值,所以错误
                if(tNode.getDeep()==parent.getDeep()+1){       //当前父节点的的直接子节点
                    if(!parent.setChildType(ChildType.NODE)) return pContent;
                }else if(tNode.getDeep()==parent.getDeep()+2){ //当前父节点的的直接孙子节点
                    if(!now.setChildType(ChildType.NODE)) return pContent;
                    parent=now;
                }else if(tNode.getDeep()<parent.getDeep()+1){  //上级节点的子节点
                    do{
                        if((parent=parent.getParent())==null) return pContent;    //错误,
                    }while(tNode.getDeep()<parent.getDeep()+1);
                }else return pContent;   //错误
                tNode.setParent(parent);
                now=tNode;
                //设置注释,以及判断节点是否闭合
                this.addCommentToContent(contents,tNode.getDeep()*tIndent,this.getComments(tNode.getPath()));;
                contents.add(sLine);
                while(!tNode.mClosed){
                    if(i>=allLines.length-1) return pContent;  //读取完毕
                    contents.add(allLines[++i]);
                    if(LineAnalyse.hasCloseMark(allLines[i],tWrapper)) break;
                }
                continue;
            }
            if((matcher=CommentedYamlConfig.CFG_VALUE.matcher(sLine)).find()){//值节点
                if(!now.setChildType(ChildType.ARRAY)) return pContent;
                int blackCount=matcher.group(1).length();
                if(blackCount!=now.getDeep()*tIndent) return pContent;  //错误,不在同一级
                boolean tClose=LineAnalyse.isCloseLine(matcher.group(2),tWrapper);
                contents.add(sLine);
                while(!tClose){
                    if(i>=allLines.length-1) return pContent;  //读取完毕,仍然没有关闭
                    contents.add(allLines[++i]);
                    if(LineAnalyse.hasCloseMark(allLines[i],tWrapper)) break;
                }
                continue;
            }
            return pContent; //不认识的节点,或者处理的时候出错了
        }
        StringBuilder builder=new StringBuilder();
        for(String sStr : contents){
            builder.append(sStr);
            builder.append(System.getProperty("line.separator", "\n"));
        }
        return builder.toString();
    }
    
    private void addCommentToContent(ArrayList<String> contents,int blackCount,ArrayList<String> comments){
        if(comments==null||comments.size()==0) return;
        String blackPrefix="";
        for(int i=0;i<blackCount;i++) blackPrefix+=" ";
        blackPrefix+="# ";
        for(String commentLine : comments)
            contents.add(blackPrefix+commentLine);
    }
    
}
