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

package com.actiontech.dble.plan.common.item;

import com.actiontech.dble.backend.mysql.CharsetUtil;
import com.actiontech.dble.config.ErrorCode;
import com.actiontech.dble.net.mysql.FieldPacket;
import com.actiontech.dble.plan.NamedField;
import com.actiontech.dble.plan.common.context.NameResolutionContext;
import com.actiontech.dble.plan.common.context.ReferContext;
import com.actiontech.dble.plan.common.exception.MySQLOutPutException;
import com.actiontech.dble.plan.common.field.Field;
import com.actiontech.dble.plan.common.time.MySQLTime;
import com.actiontech.dble.plan.node.JoinNode;
import com.actiontech.dble.plan.node.PlanNode;
import com.actiontech.dble.plan.node.PlanNode.PlanNodeType;
import com.actiontech.dble.util.StringUtil;
import com.alibaba.druid.sql.ast.SQLExpr;
import com.alibaba.druid.sql.ast.expr.SQLIdentifierExpr;
import com.alibaba.druid.sql.ast.expr.SQLPropertyExpr;
import org.apache.commons.lang.StringUtils;

import java.io.UnsupportedEncodingException;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.List;

public class ItemField extends ItemIdent {

    private Field field;

    /* if index!=-1, means the index of Item_field,need setField before val */
    private int index = -1;

    private boolean isNatualShow = true; // when natual join ,use it

    public ItemField(String dbName, String tableName, String fieldName, int charsetIndex) {
        super(dbName, tableName, fieldName);
        this.charsetIndex = charsetIndex;
    }

    public ItemField(String dbName, String tableName, String fieldName) {
        super(dbName, tableName, fieldName);
    }

    public ItemField(Field field) {
        super(null, field.getTable(), field.getName());
        setField(field);
    }

    /**
     * save index
     *
     * @param index
     */
    public ItemField(int index) {
        super(null, "", "");
        this.index = index;
    }



    public void setField(List<Field> fields, int num) {
        assert (fields != null);
        this.field = fields.get(num);
    }

    protected void setField(Field field) {
        this.field = field;
        maybeNull = field.maybeNull();
        decimals = field.getDecimals();
        tableName = field.getTable();
        itemName = field.getName();
        dbName = field.getDbName();
        maxLength = field.getFieldLength();
        charsetIndex = field.getCharsetIndex();
        fixed = true;
    }

    @Override
    public ItemType type() {
        return ItemType.FIELD_ITEM;
    }

    @Override
    public ItemResult resultType() {
        return field == null ? null : field.resultType();
    }

    @Override
    public ItemResult numericContextResultType() {
        return field.numericContextResultType();
    }

    @Override
    public FieldTypes fieldType() {
        return field.fieldType();
    }

    @Override
    public byte[] getRowPacketByte() {
        return field.getPtr();
    }

    public ItemResult cmpType() {
        return field.cmpType();
    }

    @Override
    public int hashCode() {
        int prime = 31;
        int hashCode = dbName == null ? 0 : dbName.hashCode();
        hashCode = hashCode * prime + (tableName == null ? 0 : tableName.hashCode());
        hashCode = hashCode * prime + (itemName == null ? 0 : itemName.hashCode());
        return hashCode;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (!(obj instanceof ItemField))
            return false;
        ItemField other = (ItemField) obj;
        return StringUtils.equals(getTableName(), other.getTableName()) &&
                StringUtils.equalsIgnoreCase(getItemName(), other.getItemName());
    }

    @Override
    public BigDecimal valReal() {
        if (nullValue = field.isNull())
            return BigDecimal.ZERO;
        return field.valReal();
    }

    @Override
    public BigInteger valInt() {
        if (nullValue = field.isNull())
            return BigInteger.ZERO;
        return field.valInt();
    }

    @Override
    public long valTimeTemporal() {
        if ((nullValue = field.isNull()))
            return 0;
        return field.valTimeTemporal();
    }

    @Override
    public long valDateTemporal() {
        if ((nullValue = field.isNull()))
            return 0;
        return field.valDateTemporal();
    }

    @Override
    public BigDecimal valDecimal() {
        if (nullValue = field.isNull())
            return null;
        return field.valDecimal();
    }

    @Override
    public String valStr() {
        if (nullValue = field.isNull())
            return null;
        return field.valStr();
    }

    @Override
    public boolean getDate(MySQLTime ltime, long fuzzydate) {
        if ((nullValue = field.isNull()) || field.getDate(ltime, fuzzydate)) {
            ltime.setZeroTime(ltime.getTimeType());
            return true;
        }
        return false;
    }

    @Override
    public boolean getTime(MySQLTime ltime) {
        if ((nullValue = field.isNull()) || field.getTime(ltime)) {
            ltime.setZeroTime(ltime.getTimeType());
            return true;
        }
        return false;
    }

    @Override
    public boolean isNull() {
        return field.isNull();
    }

    @Override
    public void makeField(FieldPacket fp) {
        field.makeField(fp);
        try {
            if (itemName != null) {
                fp.setName(itemName.getBytes(CharsetUtil.getJavaCharset(charsetIndex)));
            }
            if ((tableName != null)) {
                fp.setTable(tableName.getBytes(CharsetUtil.getJavaCharset(charsetIndex)));
            }
            if (dbName != null) {
                fp.setDb(dbName.getBytes(CharsetUtil.getJavaCharset(charsetIndex)));
            }
        } catch (UnsupportedEncodingException e) {
            LOGGER.info("parse string exception!", e);
        }
    }

    public String getTableName() {
        return tableName;
    }



    @Override
    public boolean fixFields() {
        assert (!fixed);
        if (!fixed) {
            setField(this.field);
        }
        return false;
    }

    @Override
    public Item fixFields(NameResolutionContext context) {
        if (this.isWild())
            return this;
        String tmpFieldName = getItemName();
        PlanNode planNode = context.getPlanNode();
        if (context.getPlanNode().type() == PlanNodeType.MERGE) {
            return getMergeNodeColumn(tmpFieldName, planNode);
        }
        Item column = null;
        if (context.isFindInSelect()) {
            // try to find in selectlist
            if (StringUtils.isEmpty(getDbName()) || StringUtils.isEmpty(getTableName())) {
                for (NamedField namedField : planNode.getOuterFields().keySet()) {
                    if (StringUtils.equalsIgnoreCase(tmpFieldName, namedField.getName()) &&
                            (StringUtils.isEmpty(getTableName()) || (StringUtils.isEmpty(getDbName()) && StringUtils.equals(getTableName(), namedField.getTable())))) {
                        if (column == null) {
                            column = planNode.getOuterFields().get(namedField);
                        } else
                            throw new MySQLOutPutException(ErrorCode.ER_OPTIMIZER, "42S22", "duplicate column:" + this);
                    }
                }
            } else {
                column = planNode.getOuterFields().get(new NamedField(getDbName(), getTableName(), tmpFieldName, null));
            }
        }
        if (column != null && context.isSelectFirst()) {
            return column;
        }
        return findItemFormInnerField(tmpFieldName, planNode, column);
    }

    private Item findItemFormInnerField(String tmpFieldName, PlanNode planNode, Item column) {
        // find from inner fields
        Item columnFromMeta = null;
        if (StringUtils.isEmpty(getDbName()) || StringUtils.isEmpty(getTableName())) {
            String tmpDbName = null;
            String tmpTableName = null;
            for (NamedField namedField : planNode.getInnerFields().keySet()) {
                if (StringUtils.equalsIgnoreCase(tmpFieldName, namedField.getName()) &&
                        (StringUtils.isEmpty(getTableName()) || (StringUtils.isEmpty(getDbName()) && StringUtils.equals(getTableName(), namedField.getTable())))) {
                    if (columnFromMeta == null) {
                        tmpDbName = namedField.getSchema();
                        tmpTableName = namedField.getTable();
                        getReferTables().clear();
                        NamedField coutField = planNode.getInnerFields().get(new NamedField(namedField.getSchema(), namedField.getTable(), tmpFieldName, null));
                        this.getReferTables().add(coutField.planNode);
                        columnFromMeta = this;
                    } else {
                        if (planNode.type() == PlanNodeType.JOIN) {
                            JoinNode jn = (JoinNode) planNode;
                            if (jn.getUsingFields() != null && jn.getUsingFields().contains(columnFromMeta.getItemName().toLowerCase())) {
                                continue;
                            }
                            throw new MySQLOutPutException(ErrorCode.ER_NON_UNIQ_ERROR, "23000", "Column '" + tmpFieldName + "' in field list is ambiguous");
                        }
                        throw new MySQLOutPutException(ErrorCode.ER_OPTIMIZER, "42S22", "duplicate column:" + this);
                    }
                }
            }
            this.dbName = tmpDbName;
            this.tableName = tmpTableName;
        } else {
            NamedField tmpField = new NamedField(getDbName(), getTableName(), tmpFieldName, null);
            if (planNode.getInnerFields().containsKey(tmpField)) {
                NamedField coutField = planNode.getInnerFields().get(tmpField);
                getReferTables().clear();
                getReferTables().add(coutField.planNode);
                this.dbName = tmpField.getSchema();
                this.tableName = tmpField.getTable();
                columnFromMeta = this;
            }
        }
        if (columnFromMeta != null) {
            return columnFromMeta;
        } else if (column == null)
            throw new MySQLOutPutException(ErrorCode.ER_OPTIMIZER, "42S22", "column " + this + " not found");
        else {
            return column;
        }

    }

    private Item getMergeNodeColumn(String tmpFieldName, PlanNode planNode) {
        String tmpFieldTable = null;
        // select union only found in outerfields
        Item column;
        if (StringUtils.isEmpty(getTableName())) {
            PlanNode firstNode = planNode.getChild();
            boolean found = false;
            for (NamedField coutField : firstNode.getOuterFields().keySet()) {
                if (tmpFieldName.equalsIgnoreCase(coutField.getName())) {
                    if (!found) {
                        tmpFieldTable = coutField.getTable();
                        found = true;
                    } else {
                        throw new MySQLOutPutException(ErrorCode.ER_BAD_FIELD_ERROR, "(42S22",
                                "Unknown column '" + tmpFieldName + "' in 'order clause'");
                    }
                }
            }
            column = planNode.getOuterFields().get(new NamedField(null, tmpFieldTable, tmpFieldName, null));
        } else {
            throw new MySQLOutPutException(ErrorCode.ER_TABLENAME_NOT_ALLOWED_HERE, "42000",
                    "Table '" + getTableName() + "' from one of the SELECTs cannot be used in global ORDER clause");
        }
        return column;
    }

    @Override
    public void fixRefer(ReferContext context) {
        if (isWild())
            return;
        PlanNode node = context.getPlanNode();
        PlanNode tn = getReferTables().iterator().next();
        node.addSelToReferedMap(tn, this);
    }

    @Override
    public SQLExpr toExpression() {
        SQLIdentifierExpr parent = StringUtil.isEmpty(tableName) ? null : new SQLIdentifierExpr(tableName);
        if (parent != null) {
            return new SQLPropertyExpr(parent, itemName);
        } else return new SQLIdentifierExpr(itemName);
    }

    @Override
    protected Item cloneStruct(boolean forCalculate, List<Item> calArgs, boolean isPushDown, List<Field> fields) {
        return new ItemField(dbName, tableName, itemName, charsetIndex);
    }

    public Field getField() {
        return field;
    }

    public void noNatualShow() {
        isNatualShow = false;
    }

    public boolean isNatualShow() {
        return isNatualShow;
    }
}
