/*
 * Copyright (C) 2008 The Android Open Source Project
 *
 * 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 com.android.ddmlib.log;

import com.android.ddmlib.IDevice;
import com.android.ddmlib.Log;
import com.android.ddmlib.MultiLineReceiver;
import com.android.ddmlib.log.EventContainer.EventValueType;
import com.android.ddmlib.log.EventValueDescription.ValueType;
import com.android.ddmlib.log.LogReceiver.LogEntry;
import com.android.ddmlib.utils.ArrayHelper;
import com.google.common.base.Charsets;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Parser for the "event" log.
 */
public final class EventLogParser {

    /** Location of the tag map file on the device */
    private static final String EVENT_TAG_MAP_FILE = "/system/etc/event-log-tags"; //$NON-NLS-1$

    /**
     * Event log entry types.  These must match up with the declarations in
     * java/android/android/util/EventLog.java.
     */
    private static final int EVENT_TYPE_INT      = 0;
    private static final int EVENT_TYPE_LONG     = 1;
    private static final int EVENT_TYPE_STRING   = 2;
    private static final int EVENT_TYPE_LIST     = 3;

    private static final Pattern PATTERN_SIMPLE_TAG = Pattern.compile(
    "^(\\d+)\\s+([A-Za-z0-9_]+)\\s*$"); //$NON-NLS-1$
    private static final Pattern PATTERN_TAG_WITH_DESC = Pattern.compile(
            "^(\\d+)\\s+([A-Za-z0-9_]+)\\s*(.*)\\s*$"); //$NON-NLS-1$
    private static final Pattern PATTERN_DESCRIPTION = Pattern.compile(
            "\\(([A-Za-z0-9_\\s]+)\\|(\\d+)(\\|\\d+){0,1}\\)"); //$NON-NLS-1$

    private static final Pattern TEXT_LOG_LINE = Pattern.compile(
            "(\\d\\d)-(\\d\\d)\\s(\\d\\d):(\\d\\d):(\\d\\d).(\\d{3})\\s+I/([a-zA-Z0-9_]+)\\s*\\(\\s*(\\d+)\\):\\s+(.*)"); //$NON-NLS-1$

    private final TreeMap<Integer, String> mTagMap = new TreeMap<Integer, String>();

    private final TreeMap<Integer, EventValueDescription[]> mValueDescriptionMap =
        new TreeMap<Integer, EventValueDescription[]>();

    public EventLogParser() {
    }

    /**
     * Inits the parser for a specific Device.
     * <p>
     * This methods reads the event-log-tags located on the device to find out
     * what tags are being written to the event log and what their format is.
     * @param device The device.
     * @return <code>true</code> if success, <code>false</code> if failure or cancellation.
     */
    public boolean init(IDevice device) {
        // read the event tag map file on the device.
        try {
            device.executeShellCommand("cat " + EVENT_TAG_MAP_FILE, //$NON-NLS-1$
                    new MultiLineReceiver() {
                @Override
                public void processNewLines(String[] lines) {
                    for (String line : lines) {
                        processTagLine(line);
                    }
                }
                @Override
                public boolean isCancelled() {
                    return false;
                }
            });
        } catch (Exception e) {
            // catch all possible exceptions and return false.
            return false;
        }

        return true;
    }

    /**
     * Inits the parser with the content of a tag file.
     * @param tagFileContent the lines of a tag file.
     * @return <code>true</code> if success, <code>false</code> if failure.
     */
    public boolean init(String[] tagFileContent) {
        for (String line : tagFileContent) {
            processTagLine(line);
        }
        return true;
    }

    /**
     * Inits the parser with a specified event-log-tags file.
     * @param filePath
     * @return <code>true</code> if success, <code>false</code> if failure.
     */
    public boolean init(String filePath)  {
        BufferedReader reader = null;
        try {
            reader = new BufferedReader(new FileReader(filePath));

            String line = null;
            do {
                line = reader.readLine();
                if (line != null) {
                    processTagLine(line);
                }
            } while (line != null);

            return true;
        } catch (IOException e) {
            return false;
        } finally {
            try {
                if (reader != null) {
                    reader.close();
                }
            } catch (IOException e) {
                // ignore
            }
        }
    }

    /**
     * Processes a line from the event-log-tags file.
     * @param line the line to process
     */
    private void processTagLine(String line) {
        // ignore empty lines and comment lines
        if (!line.isEmpty() && line.charAt(0) != '#') {
            Matcher m = PATTERN_TAG_WITH_DESC.matcher(line);
            if (m.matches()) {
                try {
                    int value = Integer.parseInt(m.group(1));
                    String name = m.group(2);
                    if (name != null && mTagMap.get(value) == null) {
                        mTagMap.put(value, name);
                    }

                    // special case for the GC tag. We ignore what is in the file,
                    // and take what the custom GcEventContainer class tells us.
                    // This is due to the event encoding several values on 2 longs.
                    // @see GcEventContainer
                    if (value == GcEventContainer.GC_EVENT_TAG) {
                        mValueDescriptionMap.put(value,
                            GcEventContainer.getValueDescriptions());
                    } else {

                        String description = m.group(3);
                        if (description != null && !description.isEmpty()) {
                            EventValueDescription[] desc =
                                processDescription(description);

                            if (desc != null) {
                                mValueDescriptionMap.put(value, desc);
                            }
                        }
                    }
                } catch (NumberFormatException e) {
                    // failed to convert the number into a string. just ignore it.
                }
            } else {
                m = PATTERN_SIMPLE_TAG.matcher(line);
                if (m.matches()) {
                    int value = Integer.parseInt(m.group(1));
                    String name = m.group(2);
                    if (name != null && mTagMap.get(value) == null) {
                        mTagMap.put(value, name);
                    }
                }
            }
        }
    }

    private EventValueDescription[] processDescription(String description) {
        String[] descriptions = description.split("\\s*,\\s*"); //$NON-NLS-1$

        ArrayList<EventValueDescription> list = new ArrayList<EventValueDescription>();

        for (String desc : descriptions) {
            Matcher m = PATTERN_DESCRIPTION.matcher(desc);
            if (m.matches()) {
                try {
                    String name = m.group(1);

                    String typeString = m.group(2);
                    int typeValue = Integer.parseInt(typeString);
                    EventValueType eventValueType = EventValueType.getEventValueType(typeValue);
                    if (eventValueType == null) {
                        // just ignore this description if the value is not recognized.
                        // TODO: log the error.
                    }

                    typeString = m.group(3);
                    if (typeString != null && !typeString.isEmpty()) {
                        //skip the |
                        typeString = typeString.substring(1);

                        typeValue = Integer.parseInt(typeString);
                        ValueType valueType = ValueType.getValueType(typeValue);

                        list.add(new EventValueDescription(name, eventValueType, valueType));
                    } else {
                        list.add(new EventValueDescription(name, eventValueType));
                    }
                } catch (NumberFormatException nfe) {
                    // just ignore this description if one number is malformed.
                    // TODO: log the error.
                } catch (InvalidValueTypeException e) {
                    // just ignore this description if data type and data unit don't match
                    // TODO: log the error.
                }
            } else {
                Log.e("EventLogParser",  //$NON-NLS-1$
                    String.format("Can't parse %1$s", description));  //$NON-NLS-1$
            }
        }

        if (list.isEmpty()) {
            return null;
        }

        return list.toArray(new EventValueDescription[list.size()]);

    }

    public EventContainer parse(LogEntry entry) {
        if (entry.len < 4) {
            return null;
        }

        int inOffset = 0;

        int tagValue = ArrayHelper.swap32bitFromArray(entry.data, inOffset);
        inOffset += 4;

        String tag = mTagMap.get(tagValue);
        if (tag == null) {
            Log.e("EventLogParser", String.format("unknown tag number: %1$d", tagValue));
        }

        ArrayList<Object> list = new ArrayList<Object>();
        if (parseBinaryEvent(entry.data, inOffset, list) == -1) {
            return null;
        }

        Object data;
        if (list.size() == 1) {
            data = list.get(0);
        } else{
            data = list.toArray();
        }

        EventContainer event = null;
        if (tagValue == GcEventContainer.GC_EVENT_TAG) {
            event = new GcEventContainer(entry, tagValue, data);
        } else {
            event = new EventContainer(entry, tagValue, data);
        }

        return event;
    }

    public EventContainer parse(String textLogLine) {
        // line will look like
        // 04-29 23:16:16.691 I/dvm_gc_info(  427): <data>
        // where <data> is either
        // [value1,value2...]
        // or
        // value
        if (textLogLine.isEmpty()) {
            return null;
        }

        // parse the header first
        Matcher m = TEXT_LOG_LINE.matcher(textLogLine);
        if (m.matches()) {
            try {
                int month = Integer.parseInt(m.group(1));
                int day = Integer.parseInt(m.group(2));
                int hours = Integer.parseInt(m.group(3));
                int minutes = Integer.parseInt(m.group(4));
                int seconds = Integer.parseInt(m.group(5));
                int milliseconds = Integer.parseInt(m.group(6));

                // convert into seconds since epoch and nano-seconds.
                Calendar cal = Calendar.getInstance();
                cal.set(cal.get(Calendar.YEAR), month-1, day, hours, minutes, seconds);
                int sec = (int)Math.floor(cal.getTimeInMillis()/1000);
                int nsec = milliseconds * 1000000;

                String tag = m.group(7);

                // get the numerical tag value
                int tagValue = -1;
                Set<Entry<Integer, String>> tagSet = mTagMap.entrySet();
                for (Entry<Integer, String> entry : tagSet) {
                    if (tag.equals(entry.getValue())) {
                        tagValue = entry.getKey();
                        break;
                    }
                }

                if (tagValue == -1) {
                    return null;
                }

                int pid = Integer.parseInt(m.group(8));

                Object data = parseTextData(m.group(9), tagValue);
                if (data == null) {
                    return null;
                }

                // now we can allocate and return the EventContainer
                EventContainer event = null;
                if (tagValue == GcEventContainer.GC_EVENT_TAG) {
                    event = new GcEventContainer(tagValue, pid, -1 /* tid */, sec, nsec, data);
                } else {
                    event = new EventContainer(tagValue, pid, -1 /* tid */, sec, nsec, data);
                }

                return event;
            } catch (NumberFormatException e) {
                return null;
            }
        }

        return null;
    }

    public Map<Integer, String> getTagMap() {
        return mTagMap;
    }

    public Map<Integer, EventValueDescription[]> getEventInfoMap() {
        return mValueDescriptionMap;
    }

    /**
     * Recursively convert binary log data to printable form.
     *
     * This needs to be recursive because you can have lists of lists.
     *
     * If we run out of room, we stop processing immediately.  It's important
     * for us to check for space on every output element to avoid producing
     * garbled output.
     *
     * Returns the amount read on success, -1 on failure.
     */
    private static int parseBinaryEvent(byte[] eventData, int dataOffset, ArrayList<Object> list) {

        if (eventData.length - dataOffset < 1)
            return -1;

        int offset = dataOffset;

        int type = eventData[offset++];

        //fprintf(stderr, "--- type=%d (rem len=%d)\n", type, eventDataLen);

        switch (type) {
        case EVENT_TYPE_INT: { /* 32-bit signed int */
                int ival;

                if (eventData.length - offset < 4)
                    return -1;
                ival = ArrayHelper.swap32bitFromArray(eventData, offset);
                offset += 4;

                list.add(ival);
            }
            break;
        case EVENT_TYPE_LONG: { /* 64-bit signed long */
                long lval;

                if (eventData.length - offset < 8)
                    return -1;
                lval = ArrayHelper.swap64bitFromArray(eventData, offset);
                offset += 8;

                list.add(lval);
            }
            break;
        case EVENT_TYPE_STRING: { /* UTF-8 chars, not NULL-terminated */
                int strLen;

                if (eventData.length - offset < 4)
                    return -1;
                strLen = ArrayHelper.swap32bitFromArray(eventData, offset);
                offset += 4;

                if (eventData.length - offset < strLen)
                    return -1;

                // get the string
                String str = new String(eventData, offset, strLen, Charsets.UTF_8);
                list.add(str);
                offset += strLen;
                break;
            }
        case EVENT_TYPE_LIST: { /* N items, all different types */

                if (eventData.length - offset < 1)
                    return -1;

                int count = eventData[offset++];

                // make a new temp list
                ArrayList<Object> subList = new ArrayList<Object>();
                for (int i = 0; i < count; i++) {
                    int result = parseBinaryEvent(eventData, offset, subList);
                    if (result == -1) {
                        return result;
                    }

                    offset += result;
                }

                list.add(subList.toArray());
            }
            break;
        default:
            Log.e("EventLogParser",  //$NON-NLS-1$
                    String.format("Unknown binary event type %1$d", type));  //$NON-NLS-1$
            return -1;
        }

        return offset - dataOffset;
    }

    private Object parseTextData(String data, int tagValue) {
        // first, get the description of what we're supposed to parse
        EventValueDescription[] desc = mValueDescriptionMap.get(tagValue);

        if (desc == null) {
            // TODO parse and create string values.
            return null;
        }

        if (desc.length == 1) {
            return getObjectFromString(data, desc[0].getEventValueType());
        } else if (data.startsWith("[") && data.endsWith("]")) {
            data = data.substring(1, data.length() - 1);

            // get each individual values as String
            String[] values = data.split(",");

            if (tagValue == GcEventContainer.GC_EVENT_TAG) {
                // special case for the GC event!
                Object[] objects = new Object[2];

                objects[0] = getObjectFromString(values[0], EventValueType.LONG);
                objects[1] = getObjectFromString(values[1], EventValueType.LONG);

                return objects;
            } else {
                // must be the same number as the number of descriptors.
                if (values.length != desc.length) {
                    return null;
                }

                Object[] objects = new Object[values.length];

                for (int i = 0 ; i < desc.length ; i++) {
                    Object obj = getObjectFromString(values[i], desc[i].getEventValueType());
                    if (obj == null) {
                        return null;
                    }
                    objects[i] = obj;
                }

                return objects;
            }
        }

        return null;
    }


    private Object getObjectFromString(String value, EventValueType type) {
        try {
            switch (type) {
                case INT:
                    return Integer.valueOf(value);
                case LONG:
                    return Long.valueOf(value);
                case STRING:
                    return value;
            }
        } catch (NumberFormatException e) {
            // do nothing, we'll return null.
        }

        return null;
    }

    /**
     * Recreates the event-log-tags at the specified file path.
     * @param filePath the file path to write the file.
     * @throws IOException
     */
    public void saveTags(String filePath) throws IOException {
        File destFile = new File(filePath);
        destFile.createNewFile();
        FileOutputStream fos = null;

        try {

            fos = new FileOutputStream(destFile);

            for (Integer key : mTagMap.keySet()) {
                // get the tag name
                String tagName = mTagMap.get(key);

                // get the value descriptions
                EventValueDescription[] descriptors = mValueDescriptionMap.get(key);

                String line = null;
                if (descriptors != null) {
                    StringBuilder sb = new StringBuilder();
                    sb.append(String.format("%1$d %2$s", key, tagName)); //$NON-NLS-1$
                    boolean first = true;
                    for (EventValueDescription evd : descriptors) {
                        if (first) {
                            sb.append(" ("); //$NON-NLS-1$
                            first = false;
                        } else {
                            sb.append(",("); //$NON-NLS-1$
                        }
                        sb.append(evd.getName());
                        sb.append("|"); //$NON-NLS-1$
                        sb.append(evd.getEventValueType().getValue());
                        sb.append("|"); //$NON-NLS-1$
                        sb.append(evd.getValueType().getValue());
                        sb.append("|)"); //$NON-NLS-1$
                    }
                    sb.append("\n"); //$NON-NLS-1$

                    line = sb.toString();
                } else {
                    line = String.format("%1$d %2$s\n", key, tagName); //$NON-NLS-1$
                }

                byte[] buffer = line.getBytes();
                fos.write(buffer);
            }
        } finally {
            if (fos != null) {
                fos.close();
            }
        }
    }


}
