/*
 * Copyright (C) 2016-2022 ActionTech.
 * License: http://www.gnu.org/licenses/gpl.html GPL version 2 or higher.
 */

package com.actiontech.dble.meta;

import com.actiontech.dble.DbleServer;
import com.actiontech.dble.config.ErrorCode;
import com.actiontech.dble.plan.common.item.Item;
import com.actiontech.dble.plan.node.MergeNode;
import com.actiontech.dble.plan.node.PlanNode;
import com.actiontech.dble.plan.node.QueryNode;
import com.actiontech.dble.plan.node.TableNode;
import com.actiontech.dble.plan.visitor.MySQLPlanNodeVisitor;
import com.actiontech.dble.route.parser.util.DruidUtil;
import com.actiontech.dble.singleton.ProxyMeta;
import com.actiontech.dble.util.StringUtil;
import com.alibaba.druid.sql.ast.statement.SQLSelectStatement;

import java.sql.SQLException;
import java.sql.SQLNonTransientException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
 * Created by szf on 2017/9/29.
 */
public class ViewMeta {
    private String viewName;
    private String createSql;
    private String selectSql;
    private PlanNode viewQuery;

    private List<String> viewColumnMeta;
    private String schema;
    private ProxyMetaManager tmManager;
    private long timestamp;

    public ViewMeta(String schema, String createSql, ProxyMetaManager tmManager) {
        this.createSql = createSql;
        this.schema = schema;
        this.tmManager = tmManager;
    }

    public void init() throws SQLException {
        ViewMetaParser viewParser = new ViewMetaParser(createSql);
        viewParser.parseCreateView(this);
        //check if the select part has
        checkDuplicate(viewParser.getType());
        parseSelectInView();
        if (viewParser.getType() != ViewMetaParser.TYPE_CREATE_VIEW) {
            createSql = "create or replace view " + viewName + " as " + selectSql;
        }
    }

    private void parseSelectInView() throws SQLException {
        SQLSelectStatement selectStatement = (SQLSelectStatement) DruidUtil.parseMultiSQL(selectSql);
        MySQLPlanNodeVisitor msv = new MySQLPlanNodeVisitor(this.schema, 63, tmManager, false, null, null);
        msv.visit(selectStatement.getSelect().getQuery());
        PlanNode selNode = msv.getTableNode();

        boolean noShardingView = true;
        if (selNode.getReferedTableNodes().size() == 0) {
            noShardingView = false;
        }
        for (TableNode tableNode : selNode.getReferedTableNodes()) {
            if (DbleServer.getInstance().getConfig().getSchemas().get(tableNode.getSchema()).isNoSharding()) {
                if (!schema.equals(tableNode.getSchema())) {
                    noShardingView = false;
                    break;
                }
            } else {
                noShardingView = false;
                break;
            }
        }

        if (noShardingView) {
            if (viewColumnMeta == null) {
                selNode.setUpFields();
                List<Item> selectItems = selNode.getColumnsSelected();
                viewColumnMeta = new ArrayList<>(selectItems.size());
                for (Item item : selectItems) {
                    String alias = item.getAlias() == null ? item.getItemName() : item.getAlias();
                    viewColumnMeta.add(StringUtil.removeBackQuote(alias));
                }
            }
            viewQuery = new TableNode(schema, viewName, viewColumnMeta);
        } else {
            if (selNode instanceof MergeNode) {
                selNode.setUpFields();
                this.setFieldsAlias(selNode, true);
            } else {
                selNode.setUpFields();
                this.setFieldsAlias(selNode, false);
            }
            viewQuery = new QueryNode(selNode);
        }
    }

    public void addMeta(boolean isNeedPersistence) throws SQLNonTransientException {
        try {
            tmManager.addMetaLock(schema, viewName, createSql);
            if (isNeedPersistence && viewQuery instanceof QueryNode) {
                ProxyMeta.getInstance().getTmManager().getRepository().put(schema, viewName, this.createSql);
            }
            tmManager.getCatalogs().get(schema).getViewMetas().put(viewName, this);
        } finally {
            tmManager.removeMetaLock(schema, viewName);
        }
    }

    private void checkDuplicate(int type) throws SQLException {
        if (StringUtil.isEmpty(schema)) {
            String msg = "No database selected";
            throw new SQLException(msg, "3D000", ErrorCode.ER_NO_DB_ERROR);
        }
        SchemaMeta schemaMeta = tmManager.getCatalogs().get(schema);
        if (schemaMeta == null) {
            throw new SQLException("Schema '" + schema + "' doesn't exist", "42S02", ErrorCode.ER_NO_DB_ERROR);
        }
        ViewMeta viewNode = schemaMeta.getViewMetas().get(viewName);
        //.getSyncView(schema,viewName);
        TableMeta tableMeta = schemaMeta.getTableMeta(viewName);
        //if the alter table
        if (type == ViewMetaParser.TYPE_ALTER_VIEW) {
            if (viewNode == null) {
                throw new SQLException("Table '" + viewName + "' doesn't exist", "42S02", ErrorCode.ER_NO_SUCH_TABLE);
            }
        }

        if (viewColumnMeta != null) {
            Set<String> tempMap = new HashSet<String>();
            for (String t : viewColumnMeta) {
                if (tempMap.contains(t.trim())) {
                    throw new SQLException("Duplicate column name '" + t + "'", "HY000", ErrorCode.ER_WRONG_COLUMN_NAME);
                }
                tempMap.add(t.trim());
            }
        }

        // if the table with same name exists
        if (tableMeta != null) {
            throw new SQLException("Table '" + viewName + "' already exists", "42S01", ErrorCode.ER_TABLE_EXISTS_ERROR);
        }

        if (type == ViewMetaParser.TYPE_CREATE_VIEW) {
            // if the sql without replace & the view exists
            if (viewNode != null) {
                // return error because the view is exists
                throw new SQLException("Table '" + viewName + "' already exists", "42S01", ErrorCode.ER_TABLE_EXISTS_ERROR);
            }
        }
    }

    private void setFieldsAlias(PlanNode selNode, boolean isMergeNode) throws SQLException {
        if (viewColumnMeta == null) {
            List<Item> selectList = selNode.getColumnsSelected();
            Set<String> tempMap = new HashSet<>();
            for (Item t : selectList) {
                if (t.getAlias() != null) {
                    if (tempMap.contains(t.getAlias())) {
                        throw new SQLException("Duplicate column name '" + t + "'", "HY000", ErrorCode.ER_WRONG_COLUMN_NAME);
                    }
                    tempMap.add(t.getAlias());
                } else {
                    if (tempMap.contains(t.getItemName())) {
                        throw new SQLException("Duplicate column name '" + t + "'", "HY000", ErrorCode.ER_WRONG_COLUMN_NAME);
                    }
                    tempMap.add(t.getItemName());
                }
            }
            return;
        }

        List<Item> columnsSelected;
        if (isMergeNode) {
            PlanNode node = getFirstNoMergeNode(selNode);
            columnsSelected = node.getColumnsSelected();
        } else {
            columnsSelected = selNode.getColumnsSelected();
        }

        int size = columnsSelected.size();
        //check if the column number of view is same as the selectList in selectStatement
        if (viewColumnMeta.size() != size) {
            //return error
            throw new SQLException("The Column_list Size and Select_statement Size Not Match", "HY000", ErrorCode.ER_WRONG_NUMBER_OF_COLUMNS_IN_SELECT);
        }
        Item column;
        for (int i = 0; i < size; i++) {
            column = columnsSelected.get(i);
            if (!column.getItemName().equalsIgnoreCase(viewColumnMeta.get(i).trim())) {
                column.setAlias(viewColumnMeta.get(i).trim());
            }
        }
    }

    private PlanNode getFirstNoMergeNode(PlanNode selNode) {
        PlanNode node = selNode.getChildren().get(0);
        if (!(node instanceof MergeNode)) {
            return node;
        }
        return getFirstNoMergeNode(node);
    }


    public String getCreateSql() {
        return createSql;
    }

    public String getViewName() {
        return viewName;
    }

    public void setViewName(String viewName) {
        this.viewName = viewName;
    }

    public PlanNode getViewQuery() {
        return viewQuery;
    }


    public String getSelectSql() {
        return selectSql;
    }

    public void setSelectSql(String selectSql) {
        this.selectSql = selectSql;
    }

    public List<String> getViewColumnMeta() {
        return viewColumnMeta;
    }

    public void setViewColumnMeta(List<String> viewColumnMeta) {
        this.viewColumnMeta = viewColumnMeta;
    }

    public String getViewColumnMetaString() {
        if (viewColumnMeta != null) {
            StringBuffer sb = new StringBuffer("(");
            for (String s : viewColumnMeta) {
                sb.append(s);
                sb.append(",");
            }
            sb.setCharAt(sb.length() - 1, ')');
            return sb.toString();
        }
        return null;
    }

    public long getTimestamp() {
        return timestamp;
    }

    public void setTimestamp(long timestamp) {
        this.timestamp = timestamp;
    }

    public String getSchema() {
        return schema;
    }

    public void setSchema(String schema) {
        this.schema = schema;
    }
}
