/*
 * Copyright (C) 2011 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.ddmuilib.heap;

import com.android.ddmlib.NativeAllocationInfo;
import com.android.ddmlib.NativeStackCallInfo;

import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.jface.operation.IRunnableWithProgress;

import java.io.IOException;
import java.io.LineNumberReader;
import java.io.Reader;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.InputMismatchException;
import java.util.List;
import java.util.Scanner;
import java.util.regex.Pattern;

public class NativeHeapDataImporter implements IRunnableWithProgress {
    private LineNumberReader mReader;
    private int mStartLineNumber;
    private int mEndLineNumber;

    private NativeHeapSnapshot mSnapshot;

    public NativeHeapDataImporter(Reader stream) {
        mReader = new LineNumberReader(stream);
        mReader.setLineNumber(1); // start numbering at 1
    }

    public void run(IProgressMonitor monitor)
            throws InvocationTargetException, InterruptedException {
        monitor.beginTask("Importing Heap Data", IProgressMonitor.UNKNOWN);

        List<NativeAllocationInfo> allocations = new ArrayList<NativeAllocationInfo>();
        try {
            while (true) {
                String line;
                StringBuilder sb = new StringBuilder();

                // read in a sequence of lines corresponding to a single NativeAllocationInfo
                mStartLineNumber = mReader.getLineNumber();
                while ((line = mReader.readLine()) != null) {
                    if (line.trim().length() == 0) {
                        // each block of allocations end with an empty line
                        break;
                    }

                    sb.append(line);
                    sb.append('\n');
                }
                mEndLineNumber = mReader.getLineNumber();

                // parse those lines into a NativeAllocationInfo object
                String allocationBlock = sb.toString();
                if (allocationBlock.trim().length() > 0) {
                    allocations.add(getNativeAllocation(allocationBlock));
                }

                if (line == null) { // EOF
                    break;
                }
            }
        } catch (Exception e) {
            if (e.getMessage() == null) {
                e = new RuntimeException(genericErrorMessage("Unexpected Parse error"));
            }
            throw new InvocationTargetException(e);
        } finally {
            try {
                mReader.close();
            } catch (IOException e) {
                // we can ignore this exception
            }
            monitor.done();
        }

        mSnapshot = new NativeHeapSnapshot(allocations);
    }

    /** Parse a single native allocation dump. This is the complement of
     * {@link NativeAllocationInfo#toString()}.
     *
     * An allocation is of the following form:
     * Allocations: 1
     * Size: 344748
     * Total Size: 344748
     * BeginStackTrace:
     *    40069bd8    /lib/libc_malloc_leak.so --- get_backtrace --- /libc/bionic/malloc_leak.c:258
     *    40069dd8    /lib/libc_malloc_leak.so --- leak_calloc --- /libc/bionic/malloc_leak.c:576
     *    40069bd8    /lib/libc_malloc_leak.so --- 40069bd8 ---
     *    40069dd8    /lib/libc_malloc_leak.so --- 40069dd8 ---
     * EndStackTrace
     * Note that in the above stack trace, the last two lines are examples where the address
     * was not resolved.
     *
     * @param block a string of lines corresponding to a single {@code NativeAllocationInfo}
     * @return parse the input and return the corresponding {@link NativeAllocationInfo}
     * @throws InputMismatchException if there are any parse errors
     */
    private NativeAllocationInfo getNativeAllocation(String block) {
        Scanner sc = new Scanner(block);

        String kw = sc.next();
        if (!NativeAllocationInfo.ALLOCATIONS_KW.equals(kw)) {
            throw new InputMismatchException(
                    expectedKeywordErrorMessage(NativeAllocationInfo.ALLOCATIONS_KW, kw));
        }

        int allocations = sc.nextInt();

        kw = sc.next();
        if (!NativeAllocationInfo.SIZE_KW.equals(kw)) {
            throw new InputMismatchException(
                    expectedKeywordErrorMessage(NativeAllocationInfo.SIZE_KW, kw));
        }

        int size = sc.nextInt();

        kw = sc.next();
        if (!NativeAllocationInfo.TOTAL_SIZE_KW.equals(kw)) {
            throw new InputMismatchException(
                    expectedKeywordErrorMessage(NativeAllocationInfo.TOTAL_SIZE_KW, kw));
        }

        int totalSize = sc.nextInt();
        if (totalSize != size * allocations) {
            throw new InputMismatchException(
                    genericErrorMessage("Total Size does not match size * # of allocations"));
        }

        NativeAllocationInfo info = new NativeAllocationInfo(size, allocations);

        kw = sc.next();
        if (!NativeAllocationInfo.BEGIN_STACKTRACE_KW.equals(kw)) {
            throw new InputMismatchException(
                    expectedKeywordErrorMessage(NativeAllocationInfo.BEGIN_STACKTRACE_KW, kw));
        }

        List<NativeStackCallInfo> stackInfo = new ArrayList<NativeStackCallInfo>();
        Pattern endTracePattern = Pattern.compile(NativeAllocationInfo.END_STACKTRACE_KW);

        while (true) {
            long address = sc.nextLong(16);
            info.addStackCallAddress(address);

            String library = sc.next();
            sc.next();  // ignore "---"
            String method = scanTillSeparator(sc, "---");

            String filename = "";
            if (!isUnresolved(method, address)) {
                filename = sc.next();
            }

            stackInfo.add(new NativeStackCallInfo(address, library, method, filename));

            if (sc.hasNext(endTracePattern)) {
                break;
            }
        }

        info.setResolvedStackCall(stackInfo);
        return info;
    }

    private String scanTillSeparator(Scanner sc, String separator) {
        StringBuilder sb = new StringBuilder();

        while (true) {
            String token = sc.next();
            if (token.equals(separator)) {
                break;
            }

            sb.append(token);

            // We do not know the exact delimiter that was skipped over, but we know
            // that there was atleast 1 whitespace. Add a single whitespace character
            // to account for this.
            sb.append(' ');
        }

        return sb.toString().trim();
    }

    private boolean isUnresolved(String method, long address) {
        // a method is unresolved if it is just the hex representation of the address
        return Long.toString(address, 16).equals(method);
    }

    private String genericErrorMessage(String message) {
        return String.format("%1$s between lines %2$d and %3$d",
                message, mStartLineNumber, mEndLineNumber);
    }

    private String expectedKeywordErrorMessage(String expected, String actual) {
        return String.format("Expected keyword '%1$s', saw '%2$s' between lines %3$d to %4$d.",
                expected, actual, mStartLineNumber, mEndLineNumber);
    }

    public NativeHeapSnapshot getImportedSnapshot() {
        return mSnapshot;
    }
}
