/**
 * Copyright 2014 Alexey Ragozin
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.gridkit.jvmtool;

import java.io.IOException;
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.management.Attribute;
import javax.management.InstanceNotFoundException;
import javax.management.IntrospectionException;
import javax.management.MBeanAttributeInfo;
import javax.management.MBeanException;
import javax.management.MBeanInfo;
import javax.management.MBeanOperationInfo;
import javax.management.MBeanParameterInfo;
import javax.management.MBeanServerConnection;
import javax.management.ObjectName;
import javax.management.ReflectionException;
import javax.management.openmbean.CompositeData;
import javax.management.openmbean.TabularData;

public class MBeanHelper {

    public static String FORMAT_TABLE_COLUMN_WIDTH_THRESHOLD = "table.column.maxWidth";
    public static String FORMAT_COMPOSITE_FIELD_WIDTH_THRESHODL = "composite.field.maxWidth";

    private MBeanServerConnection mserver;

    private int widthThresholdTable = 40;
    private int widthThresholdComposite = 1000;

    public MBeanHelper(MBeanServerConnection connection) {
        this.mserver = connection;
    }

    public void setFormatingOption(String name, Object value) {
        if (FORMAT_TABLE_COLUMN_WIDTH_THRESHOLD.equals(name)) {
            widthThresholdTable = (Integer) value;
        }
        else if (FORMAT_COMPOSITE_FIELD_WIDTH_THRESHODL.equals(name)) {
            widthThresholdComposite = (Integer) value;
        }
    }

    /**
     * Get MBean attributes metadata
     * @param bean MBean attributes
     * @param attrs Required attributes
     * @param read Attributes must be readable
     * @param write Attributes must be writable
     * @return Map attribute name - attribute info
     */
    private Map<String, MBeanAttributeInfo> getAttributeInfos(ObjectName bean, Collection<String> attrs, boolean read, boolean write) throws IntrospectionException, ReflectionException, InstanceNotFoundException, IOException {
        MBeanInfo mbinfo = mserver.getMBeanInfo(bean);
        // Convert array to map
        Map<String, MBeanAttributeInfo> attrInfos = new HashMap<String, MBeanAttributeInfo>(attrs.size());
        for(MBeanAttributeInfo attrInfo: mbinfo.getAttributes()) {
            attrInfos.put(attrInfo.getName(), attrInfo);
        }
        // Check required attributes and their read/write flag
        for(String attr:attrs) {
            MBeanAttributeInfo ai = attrInfos.get(attr);
            if (ai == null) {
                throw new IllegalArgumentException("No such attribute '" + attr + "'");
            }
            if (read && !ai.isReadable()) {
                throw new IllegalArgumentException("Attribute '" + attr + "' is write-only");
            }
            if (write && !ai.isWritable()) {
                throw new IllegalArgumentException("Attribute '" + attr + "' is not writeable");
            }
        }

        return attrInfos;
    }

    /**
     * Get MBean attributes metadata
     * @param bean MBean attributes
     * @param attrs Required attributes
     * @return Map attribut name - attribute value
     */
    private Map<String, Object> getAttributes(ObjectName bean, Collection<String> attrs) throws InstanceNotFoundException, ReflectionException, IOException {
        Map<String, Object> attrValues = new HashMap<String, Object>(attrs.size());
        for(Attribute attr:mserver.getAttributes(bean, attrs.toArray(new String[0])).asList()) {
            attrValues.put(attr.getName(), attr.getValue());
        }
        return attrValues;
    }

    public Map<String, String> get(ObjectName bean, Collection<String> attrs) throws Exception {
        Map<String, MBeanAttributeInfo> attrInfos = getAttributeInfos(bean, attrs, true, false);
        Map<String, Object> attrRawValues = getAttributes(bean, attrs);
        Map<String, String> attrValues = new HashMap<String, String>(attrs.size());
        for(String attr: attrs) {
            String attrValue = format(attrRawValues.get(attr), attrInfos.get(attr).getType());
            attrValues.put(attr, attrValue);
        }
        return attrValues;
    }

    public void getAsTable(ObjectName bean, Collection<String> attrs, MTable table) throws Exception {
        Map<String, MBeanAttributeInfo> attrInfos = getAttributeInfos(bean, attrs, true, false);
        Map<String, Object> attrRawValues = getAttributes(bean, attrs);
        for(String attr: attrs) {
            Object v = attrRawValues.get(attr);
            List<String> tableHeader = null;
            List<String[]> tableRows = new ArrayList<String[]>();
            if (v instanceof CompositeData[]) {
                CompositeData[] td = (CompositeData[]) v;
                if (td.length == 0) {
                    continue;
                }
                List<String> header = new ArrayList<String>();
                for (String f : td[0].getCompositeType().keySet()) {
                    if (!header.contains(f)) {
                        header.add(f);
                    }
                }
                tableHeader = header;
                for (Object row : td) {
                    tableRows.add(formatRow((CompositeData) row, header));
                }
            } else if (v instanceof CompositeData) {
                CompositeData cd = (CompositeData) v;
                List<String> header = new ArrayList<String>();
                for (String f : cd.getCompositeType().keySet()) {
                    if (!header.contains(f)) {
                        header.add(f);
                    }
                }
                tableHeader = header;
                tableRows.add(formatRow(cd, header));
            } else if (v instanceof TabularData) {
                TabularData td = (TabularData) v;
                td.getTabularType().getIndexNames();
                List<String> header = new ArrayList<String>(td.getTabularType().getIndexNames());
                for (String f : td.getTabularType().getRowType().keySet()) {
                    if (!header.contains(f)) {
                        header.add(f);
                    }
                }
                tableHeader = header;
                for (Object row : td.values()) {
                    tableRows.add(formatRow((CompositeData) row, header));
                }
            } else {
                tableHeader = Collections.singletonList("Value");
                tableRows.add(new String[]{formatLine(v, attrInfos.get(attr).getType())});
            }
            List<String> tableHeader2 = new ArrayList<String>(tableHeader.size() + 2);
            tableHeader2.add(0, "MBean");
            tableHeader2.add(1, "Attribute");
            tableHeader2.addAll(tableHeader);
            String[] hdr = tableHeader2.toArray(new String[0]);
            for(String[] tableRow:tableRows) {
                List<String> tableCells = new ArrayList<String>(tableRow.length +2);
                tableCells.add(bean.getCanonicalName());
                tableCells.add(attr);
                tableCells.addAll(Arrays.asList(tableRow));
                table.append(hdr, tableCells.toArray(new String[0]));
            }
        }
    }

    public void set(ObjectName bean, String attr, String value) throws Exception {
        Map<String, MBeanAttributeInfo> attrInfos = getAttributeInfos(bean, Collections.singletonList(attr), false, true);
        Object ov = convert(value, attrInfos.get(attr).getType());
        mserver.setAttribute(bean, new Attribute(attr, ov));
    }

    public String invoke(ObjectName bean, String operation, String... params) throws InstanceNotFoundException, IntrospectionException, ReflectionException, IOException, MBeanException {
        MBeanInfo mbinfo = mserver.getMBeanInfo(bean);
        MBeanOperationInfo op = null;
        for(MBeanOperationInfo oi: mbinfo.getOperations()) {
            if (oi.getName().equalsIgnoreCase(operation) && oi.getSignature().length == params.length) {
                if (op != null) {
                    throw new IllegalArgumentException("Ambiguous " + operation + "/" + params.length + " operatition signature for " + bean);
                }
                op = oi;
            }
        }
        if (op == null) {
            throw new IllegalArgumentException("Operation " + operation + "/" + params.length + " not found for " + bean);
        }
        Object[] args = new Object[params.length];
        String[] sig = new String[params.length];
        for(int i = 0; i != params.length; ++i) {
            args[i] = convert(params[i], op.getSignature()[i].getType());
            sig[i] = op.getSignature()[i].getType();
        }
        return format(mserver.invoke(bean, op.getName(), args, sig), op.getReturnType());
    }

    private String format(Object v, String type) {
        if (type.equals("void")) {
            return null;
        }
        else if (v instanceof CompositeData[]) {
            CompositeData[] td = (CompositeData[]) v;
            if (td.length == 0) {
                return "";
            }
            List<String> header = new ArrayList<String>();
            for(String f: td[0].getCompositeType().keySet()) {
                if (!header.contains(f)) {
                    header.add(f);
                }
            }
            List<String[]> content = new ArrayList<String[]>();
            content.add(header.toArray(new String[0]));
            for(Object row: td) {
                content.add(formatRow((CompositeData)row, header));
            }
            return formatTable(content, widthThresholdTable, true);
        }
        else if (v instanceof TabularData) {
            TabularData td = (TabularData) v;
            td.getTabularType().getIndexNames();
            List<String> header = new ArrayList<String>(td.getTabularType().getIndexNames());
            for(String f: td.getTabularType().getRowType().keySet()) {
                if (!header.contains(f)) {
                    header.add(f);
                }
            }
            List<String[]> content = new ArrayList<String[]>();
            content.add(header.toArray(new String[0]));
            for(Object row: td.values()) {
                content.add(formatRow((CompositeData)row, header));
            }
            return formatTable(content, widthThresholdTable, true);
        }
        else if (v instanceof CompositeData) {
            CompositeData cd = (CompositeData)v;
            List<String[]> content = new ArrayList<String[]>();
            for(String field: cd.getCompositeType().keySet()) {
                String val = formatLine(cd.get(field), cd.getCompositeType().getType(field).getClassName());
                content.add(new String[]{field + ": ", val});
            }
            return formatTable(content, widthThresholdComposite, false);
        }
        else {
            return formatLine(v, type);
        }
    }

    private String formatTable(List<String[]> content, int maxCell, boolean table) {
        int[] width = new int[content.get(0).length];
        for(String[] row: content) {
            for(int i = 0; i != row.length; ++i) {
                width[i] = Math.min(Math.max(width[i], row[i].length()), maxCell);
            }
        }

        StringBuilder sb = new StringBuilder();
        boolean header = table;
        for(String[] row: content) {
            for(int i = 0; i != width.length; ++i) {
                String cell = row[i];
                if (cell.length() > width[i]) {
                    cell = cell.substring(0, width[i] - 3) + "...";
                }
                sb.append(cell);
                for(int s = 0; s != width[i] - cell.length(); ++s) {
                    sb.append(' ');
                }
                if (table) {
                    sb.append('|');
                }
            }
            if (table) {
                sb.setLength(sb.length() - 1);
            }
            sb.append('\n');
            if (header) {
                header = false;
                for(int n: width) {
                    for(int i = 0; i != n; ++i) {
                        sb.append('-');
                    }
                    sb.append('+');
                }
                sb.setLength(sb.length() - 1);
                sb.append('\n');
            }
        }

        return sb.toString();
    }

    private String formatLine(Object v, String type) {
        if (v instanceof TabularData) {
            TabularData td = (TabularData)v;
            StringBuilder sb = new StringBuilder();
            for(Object c: td.values()) {
                sb.append(formatLine(c, td.getTabularType().getRowType().getClassName()));
                sb.append(",");
            }
            if (sb.length() > 0) {
                sb.setLength(sb.length() - 1);
            }
            return sb.toString();
        }
        if (v instanceof CompositeData[]) {
            CompositeData[] td = (CompositeData[])v;
            StringBuilder sb = new StringBuilder();
            for(Object c: td) {
                sb.append(formatLine(c, ((CompositeData)c).getCompositeType().getClassName()));
                sb.append(",");
            }
            if (sb.length() > 0) {
                sb.setLength(sb.length() - 1);
            }
            return sb.toString();
        }
        else if (v instanceof CompositeData) {
            CompositeData cdata = (CompositeData) v;
            StringBuilder sb = new StringBuilder();
            sb.append("{");
            for(String attr: cdata.getCompositeType().keySet()) {
                sb.append(attr).append("=");
                sb.append(formatLine(cdata.get(attr), cdata.getCompositeType().getType(attr).getClassName()));
                sb.append(',');
            }
            if (sb.length() > 1) {
                sb.setLength(sb.length() - 1);
            }
            sb.append("}");
            return sb.toString();
        }
        else if (v instanceof Object[]) {
            return Arrays.toString((Object[])v);
        }
        else if (v instanceof boolean[]) {
            return Arrays.toString((boolean[])v);
        }
        else if (v instanceof byte[]) {
            return Arrays.toString((byte[])v);
        }
        else if (v instanceof char[]) {
            return Arrays.toString((char[])v);
        }
        else if (v instanceof short[]) {
            return Arrays.toString((short[])v);
        }
        else if (v instanceof int[]) {
            return Arrays.toString((int[])v);
        }
        else if (v instanceof long[]) {
            return Arrays.toString((long[])v);
        }
        else if (v instanceof float[]) {
            return Arrays.toString((float[])v);
        }
        else if (v instanceof double[]) {
            return Arrays.toString((double[])v);
        }
        else {
            return String.valueOf(v);
        }
    }

    private String[] formatRow(CompositeData row, List<String> header) {
        String[] text = new String[header.size()];
        for(int i = 0; i != text.length; ++i) {
            String attr = header.get(i);
            text[i] = formatLine(row.get(attr), row.getCompositeType().getType(attr).getClassName());
        }
        return text;
    }

    private Object convert(String value, String type) {
        if (type.equals("java.lang.String")) {
            if ("<null>".equals(value.trim()))
                return null;
            return value;
        }
        if (type.equals("boolean") || type.equals("java.lang.Boolean")) {
            if (type.equals("java.lang.Boolean") && value.trim().isEmpty())
                return null;
            return Boolean.valueOf(value);
        }
        else if (type.equals("byte") || type.equals("java.lang.Byte")) {
            if (type.equals("java.lang.Byte") && value.trim().isEmpty())
                return null;
            return Byte.valueOf(value);
        }
        else if (type.equals("short") || type.equals("java.lang.Short")) {
            if (type.equals("java.lang.Short") && value.trim().isEmpty())
                return null;
            return Short.valueOf(value);
        }
        else if (type.equals("char") || type.equals("java.lang.Character")) {
            if (type.equals("java.lang.Character") && value.trim().isEmpty())
                return null;
            if (value.length() == 1) {
                return value.charAt(0);
            }
            else {
                throw new IllegalArgumentException("Cannot convert '" + value + "' to " + type);
            }
        }
        else if (type.equals("int") || type.equals("java.lang.Integer")) {
            if (type.equals("java.lang.Integer") && value.trim().isEmpty())
                return null;
            return Integer.valueOf(value);
        }
        else if (type.equals("long") || type.equals("java.lang.Long")) {
            if (type.equals("java.lang.Long") && value.trim().isEmpty())
                return null;
            return Long.valueOf(value);
        }
        else if (type.equals("float") || type.equals("java.lang.Float")) {
            if (type.equals("java.lang.Float") && value.trim().isEmpty())
                return null;
            return Float.valueOf(value);
        }
        else if (type.equals("double") || type.equals("java.lang.Double")) {
            if (type.equals("java.lang.Double") && value.trim().isEmpty())
                return null;
            return Double.valueOf(value);
        }
        else if (type.startsWith("[")) {
            if (value.trim().isEmpty())
                return null;
            String[] elements = value.split("[,]");
            Object array = ARRAY_MAP.get(type);
            if (array == null) {
                throw new IllegalArgumentException("Cannot convert '" + value + "' to " + type);
            }
            array = Array.newInstance(array.getClass().getComponentType(), elements.length);
            String etype = array.getClass().getComponentType().getName();
            for(int i = 0; i != elements.length; ++i) {
                Array.set(array, i, convert(elements[i], etype));
            }
            return array;
        }
        throw new IllegalArgumentException("Cannot convert '" + value + "' to " + type);
    }

    public String describe(ObjectName bean) throws Exception {
        MBeanInfo mbinfo = mserver.getMBeanInfo(bean);
        StringBuilder sb = new StringBuilder();
        sb.append(bean);
        sb.append('\n');
        sb.append(mbinfo.getClassName());
        sb.append('\n');
        sb.append(" - " + mbinfo.getDescription());
        sb.append('\n');
        for(MBeanAttributeInfo ai: mbinfo.getAttributes()) {
            sb.append(" (A) ");
            sb.append(ai.getName()).append(" : ").append(toPrintableType(ai.getType())).append("");
            if (!ai.isReadable()) {
                sb.append(" - WRITEONLY");
            }
            else if (ai.isWritable()) {
                sb.append(" - WRITEABLE");
            }
            sb.append('\n');
            if (!ai.getName().equals(ai.getDescription())) {
                sb.append("  - " + ai.getDescription());
                sb.append('\n');
            }
        }
        for (MBeanOperationInfo oi: mbinfo.getOperations()) {
            sb.append(" (O) ");
            sb.append(oi.getName()).append("(");
            for(MBeanParameterInfo pi: oi.getSignature()) {
                String name = pi.getName();
                String type = toPrintableType(pi.getType());
                sb.append(type).append(' ').append(name).append(", ");
            }
            if (oi.getSignature().length > 0) {
                sb.setLength(sb.length() - 2);
            }
            sb.append(") : ").append(toPrintableType(oi.getReturnType()));
            sb.append('\n');
            if (!oi.getName().equals(oi.getDescription())) {
                sb.append("  - " + oi.getDescription());
                sb.append('\n');
            }
        }
        return sb.toString();
    }

    static Map<String, Object> ARRAY_MAP = new HashMap<String, Object>();
    static {
        ARRAY_MAP.put("[Z", new boolean[0]);
        ARRAY_MAP.put("[B", new byte[0]);
        ARRAY_MAP.put("[S", new short[0]);
        ARRAY_MAP.put("[C", new char[0]);
        ARRAY_MAP.put("[I", new int[0]);
        ARRAY_MAP.put("[J", new long[0]);
        ARRAY_MAP.put("[F", new float[0]);
        ARRAY_MAP.put("[D", new double[0]);
        ARRAY_MAP.put("[Ljava.lang.String;", new String[0]);
    }

    static Map<String, String> TYPE_MAP = new HashMap<String, String>();
    static {
        TYPE_MAP.put("java.lang.String", "String");
        TYPE_MAP.put("javax.management.openmbean.CompositeData", "CompositeData");
        TYPE_MAP.put("javax.management.openmbean.TabularData", "TabularData");
        TYPE_MAP.put("[Z", "boolean[]");
        TYPE_MAP.put("[B", "byte[]");
        TYPE_MAP.put("[S", "short[]");
        TYPE_MAP.put("[C", "char[]");
        TYPE_MAP.put("[I", "int[]");
        TYPE_MAP.put("[J", "long[]");
        TYPE_MAP.put("[F", "float[]");
        TYPE_MAP.put("[D", "double[]");
    }

    static String toPrintableType(String type) {
        if (TYPE_MAP.containsKey(type)) {
            return TYPE_MAP.get(type);
        }
        else if (type.startsWith("[L")) {
            return toPrintableType(type.substring(2, type.length() -1)) + "[]";
        }
        else {
            return type;
        }
    }

}
