/*
 * Copyright (c) 2001, 2013, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package com.sun.tools.jdi;

import com.sun.jdi.*;

import java.util.*;
import java.io.File;

class SDE {
    private static final int INIT_SIZE_FILE = 3;
    private static final int INIT_SIZE_LINE = 100;
    private static final int INIT_SIZE_STRATUM = 3;

    static final String BASE_STRATUM_NAME = "Java";

    /* for C capatibility */
    static final String NullString = null;

    private class FileTableRecord {
        int fileId;
        String sourceName;
        String sourcePath; // do not read - use accessor
        boolean isConverted = false;

        /**
         * Return the sourcePath, computing it if not set.
         * If set, convert '/' in the sourcePath to the
         * local file separator.
         */
        String getSourcePath(ReferenceTypeImpl refType) {
            if (!isConverted) {
                if (sourcePath == null) {
                    sourcePath = refType.baseSourceDir() + sourceName;
                } else {
                    StringBuffer buf = new StringBuffer();
                    for (int i = 0; i < sourcePath.length(); ++i) {
                        char ch = sourcePath.charAt(i);
                        if (ch == '/') {
                            buf.append(File.separatorChar);
                        } else {
                            buf.append(ch);
                        }
                    }
                    sourcePath = buf.toString();
                }
                isConverted = true;
            }
            return sourcePath;
        }
    }

    private class LineTableRecord {
        int jplsStart;
        int jplsEnd;
        int jplsLineInc;
        int njplsStart;
        int njplsEnd;
        int fileId;
    }

    private class StratumTableRecord {
        String id;
        int fileIndex;
        int lineIndex;
    }

    class Stratum {
        private final int sti; /* stratum index */

        private Stratum(int sti) {
            this.sti = sti;
        }

        String id() {
            return stratumTable[sti].id;
        }

        boolean isJava() {
            return sti == baseStratumIndex;
        }

        /**
         * Return all the sourceNames for this stratum.
         * Look from our starting fileIndex upto the starting
         * fileIndex of next stratum - can do this since there
         * is always a terminator stratum.
         * Default sourceName (the first one) must be first.
         */
        List<String> sourceNames(ReferenceTypeImpl refType) {
            int i;
            int fileIndexStart = stratumTable[sti].fileIndex;
            /* one past end */
            int fileIndexEnd = stratumTable[sti+1].fileIndex;
            List<String> result = new ArrayList<String>(fileIndexEnd - fileIndexStart);
            for (i = fileIndexStart; i < fileIndexEnd; ++i) {
                result.add(fileTable[i].sourceName);
            }
            return result;
        }

        /**
         * Return all the sourcePaths for this stratum.
         * Look from our starting fileIndex upto the starting
         * fileIndex of next stratum - can do this since there
         * is always a terminator stratum.
         * Default sourcePath (the first one) must be first.
         */
        List<String> sourcePaths(ReferenceTypeImpl refType) {
            int i;
            int fileIndexStart = stratumTable[sti].fileIndex;
            /* one past end */
            int fileIndexEnd = stratumTable[sti+1].fileIndex;
            List<String> result = new ArrayList<String>(fileIndexEnd - fileIndexStart);
            for (i = fileIndexStart; i < fileIndexEnd; ++i) {
                result.add(fileTable[i].getSourcePath(refType));
            }
            return result;
        }

        LineStratum lineStratum(ReferenceTypeImpl refType,
                                int jplsLine) {
            int lti = stiLineTableIndex(sti, jplsLine);
            if (lti < 0) {
                return null;
            } else {
                return new LineStratum(sti, lti, refType,
                                       jplsLine);
            }
        }
    }

    class LineStratum {
        private final int sti; /* stratum index */
        private final int lti; /* line table index */
        private final ReferenceTypeImpl refType;
        private final int jplsLine;
        private String sourceName = null;
        private String sourcePath = null;

        private LineStratum(int sti, int lti,
                            ReferenceTypeImpl refType,
                            int jplsLine) {
            this.sti = sti;
            this.lti = lti;
            this.refType = refType;
            this.jplsLine = jplsLine;
        }

        public boolean equals(Object obj) {
            if (obj instanceof LineStratum) {
                LineStratum other = (LineStratum)obj;
                return (lti == other.lti) &&
                       (sti == other.sti) &&
                       (lineNumber() == other.lineNumber()) &&
                       (refType.equals(other.refType));
            } else {
                return false;
            }
        }

        @Override
        public int hashCode() {
            return (lineNumber() * 17) ^ refType.hashCode();
        }

        int lineNumber() {
            return stiLineNumber(sti, lti, jplsLine);
        }

        /**
         * Fetch the source name and source path for
         * this line, converting or constructing
         * the source path if needed.
         */
        void getSourceInfo() {
            if (sourceName != null) {
                // already done
                return;
            }
            int fti = stiFileTableIndex(sti, lti);
            if (fti == -1) {
                throw new InternalError(
              "Bad SourceDebugExtension, no matching source id " +
              lineTable[lti].fileId + " jplsLine: " + jplsLine);
            }
            FileTableRecord ftr = fileTable[fti];
            sourceName = ftr.sourceName;
            sourcePath = ftr.getSourcePath(refType);
        }

        String sourceName() {
            getSourceInfo();
            return sourceName;
        }

        String sourcePath() {
            getSourceInfo();
            return sourcePath;
        }
    }

    private FileTableRecord[] fileTable = null;
    private LineTableRecord[] lineTable = null;
    private StratumTableRecord[] stratumTable = null;

    private int fileIndex = 0;
    private int lineIndex = 0;
    private int stratumIndex = 0;
    private int currentFileId = 0;

    private int defaultStratumIndex = -1;
    private int baseStratumIndex = -2; /* so as not to match -1 above */
    private int sdePos = 0;

    final String sourceDebugExtension;
    String jplsFilename = null;
    String defaultStratumId = null;
    boolean isValid = false;

    SDE(String sourceDebugExtension) {
        this.sourceDebugExtension = sourceDebugExtension;
        decode();
    }

    SDE() {
        this.sourceDebugExtension = null;
        createProxyForAbsentSDE();
    }

    char sdePeek() {
        if (sdePos >= sourceDebugExtension.length()) {
            syntax();
        }
        return sourceDebugExtension.charAt(sdePos);
    }

    char sdeRead() {
        if (sdePos >= sourceDebugExtension.length()) {
            syntax();
        }
        return sourceDebugExtension.charAt(sdePos++);
    }

    void sdeAdvance() {
        sdePos++;
    }

    void syntax() {
        throw new InternalError("bad SourceDebugExtension syntax - position " +
                                sdePos);
    }

    void syntax(String msg) {
        throw new InternalError("bad SourceDebugExtension syntax: " + msg);
    }

    void assureLineTableSize() {
        int len = lineTable == null? 0 : lineTable.length;
        if (lineIndex >= len) {
            int i;
            int newLen = len == 0? INIT_SIZE_LINE : len * 2;
            LineTableRecord[] newTable = new LineTableRecord[newLen];
            for (i = 0; i < len; ++i) {
                newTable[i] = lineTable[i];
            }
            for (; i < newLen; ++i) {
                newTable[i] = new LineTableRecord();
            }
            lineTable = newTable;
        }
    }

    void assureFileTableSize() {
        int len = fileTable == null? 0 : fileTable.length;
        if (fileIndex >= len) {
            int i;
            int newLen = len == 0? INIT_SIZE_FILE : len * 2;
            FileTableRecord[] newTable = new FileTableRecord[newLen];
            for (i = 0; i < len; ++i) {
                newTable[i] = fileTable[i];
            }
            for (; i < newLen; ++i) {
                newTable[i] = new FileTableRecord();
            }
            fileTable = newTable;
        }
    }

    void assureStratumTableSize() {
        int len = stratumTable == null? 0 : stratumTable.length;
        if (stratumIndex >= len) {
            int i;
            int newLen = len == 0? INIT_SIZE_STRATUM : len * 2;
            StratumTableRecord[] newTable = new StratumTableRecord[newLen];
            for (i = 0; i < len; ++i) {
                newTable[i] = stratumTable[i];
            }
            for (; i < newLen; ++i) {
                newTable[i] = new StratumTableRecord();
            }
            stratumTable = newTable;
        }
    }

    String readLine() {
        StringBuffer sb = new StringBuffer();
        char ch;

        ignoreWhite();
        while (((ch = sdeRead()) != '\n') && (ch != '\r')) {
            sb.append(ch);
        }
        // check for CR LF
        if ((ch == '\r') && (sdePeek() == '\n')) {
            sdeRead();
        }
        ignoreWhite(); // leading white
        return sb.toString();
    }

    private int defaultStratumTableIndex() {
        if ((defaultStratumIndex == -1) && (defaultStratumId != null)) {
            defaultStratumIndex =
                stratumTableIndex(defaultStratumId);
        }
        return defaultStratumIndex;
    }

    int stratumTableIndex(String stratumId) {
        int i;

        if (stratumId == null) {
            return defaultStratumTableIndex();
        }
        for (i = 0; i < (stratumIndex-1); ++i) {
            if (stratumTable[i].id.equals(stratumId)) {
                return i;
            }
        }
        return defaultStratumTableIndex();
    }

    Stratum stratum(String stratumID) {
        int sti = stratumTableIndex(stratumID);
        return new Stratum(sti);
    }

    List<String> availableStrata() {
        List<String> strata = new ArrayList<String>();

        for (int i = 0; i < (stratumIndex-1); ++i) {
            StratumTableRecord rec = stratumTable[i];
            strata.add(rec.id);
        }
        return strata;
    }

/*****************************
 * below functions/methods are written to compile under either Java or C
 *
 * Needed support functions:
 *   sdePeek()
 *   sdeRead()
 *   sdeAdvance()
 *   readLine()
 *   assureLineTableSize()
 *   assureFileTableSize()
 *   assureStratumTableSize()
 *   syntax()
 *
 *   stratumTableIndex(String)
 *
 * Needed support variables:
 *   lineTable
 *   lineIndex
 *   fileTable
 *   fileIndex
 *   currentFileId
 *
 * Needed types:
 *   String
 *
 * Needed constants:
 *   NullString
 */

    void ignoreWhite() {
        char ch;

        while (((ch = sdePeek()) == ' ') || (ch == '\t')) {
            sdeAdvance();
        }
    }

    void ignoreLine() {
        char ch;

        while (((ch = sdeRead()) != '\n') && (ch != '\r')) {
        }
        /* check for CR LF */
        if ((ch == '\r') && (sdePeek() == '\n')) {
            sdeAdvance();
        }
        ignoreWhite(); /* leading white */
    }

    int readNumber() {
        int value = 0;
        char ch;

        ignoreWhite();
        while (((ch = sdePeek()) >= '0') && (ch <= '9')) {
            sdeAdvance();
            value = (value * 10) + ch - '0';
        }
        ignoreWhite();
        return value;
    }

    void storeFile(int fileId, String sourceName, String sourcePath) {
        assureFileTableSize();
        fileTable[fileIndex].fileId = fileId;
        fileTable[fileIndex].sourceName = sourceName;
        fileTable[fileIndex].sourcePath = sourcePath;
        ++fileIndex;
    }

    void fileLine() {
        int hasAbsolute = 0; /* acts as boolean */
        int fileId;
        String sourceName;
        String sourcePath = null;

        /* is there an absolute filename? */
        if (sdePeek() == '+') {
            sdeAdvance();
            hasAbsolute = 1;
        }
        fileId = readNumber();
        sourceName = readLine();
        if (hasAbsolute == 1) {
            sourcePath = readLine();
        }

        storeFile(fileId, sourceName, sourcePath);
    }

    void storeLine(int jplsStart, int jplsEnd, int jplsLineInc,
                  int njplsStart, int njplsEnd, int fileId) {
        assureLineTableSize();
        lineTable[lineIndex].jplsStart = jplsStart;
        lineTable[lineIndex].jplsEnd = jplsEnd;
        lineTable[lineIndex].jplsLineInc = jplsLineInc;
        lineTable[lineIndex].njplsStart = njplsStart;
        lineTable[lineIndex].njplsEnd = njplsEnd;
        lineTable[lineIndex].fileId = fileId;
        ++lineIndex;
    }

    /**
     * Parse line translation info.  Syntax is
     *     <NJ-start-line> [ # <file-id> ] [ , <line-count> ] :
     *                 <J-start-line> [ , <line-increment> ] CR
     */
    void lineLine() {
        int lineCount = 1;
        int lineIncrement = 1;
        int njplsStart;
        int jplsStart;

        njplsStart = readNumber();

        /* is there a fileID? */
        if (sdePeek() == '#') {
            sdeAdvance();
            currentFileId = readNumber();
        }

        /* is there a line count? */
        if (sdePeek() == ',') {
            sdeAdvance();
            lineCount = readNumber();
        }

        if (sdeRead() != ':') {
            syntax();
        }
        jplsStart = readNumber();
        if (sdePeek() == ',') {
            sdeAdvance();
            lineIncrement = readNumber();
        }
        ignoreLine(); /* flush the rest */

        storeLine(jplsStart,
                  jplsStart + (lineCount * lineIncrement) -1,
                  lineIncrement,
                  njplsStart,
                  njplsStart + lineCount -1,
                  currentFileId);
    }

    /**
     * Until the next stratum section, everything after this
     * is in stratumId - so, store the current indicies.
     */
    void storeStratum(String stratumId) {
        /* remove redundant strata */
        if (stratumIndex > 0) {
            if ((stratumTable[stratumIndex-1].fileIndex
                                            == fileIndex) &&
                (stratumTable[stratumIndex-1].lineIndex
                                            == lineIndex)) {
                /* nothing changed overwrite it */
                --stratumIndex;
            }
        }
        /* store the results */
        assureStratumTableSize();
        stratumTable[stratumIndex].id = stratumId;
        stratumTable[stratumIndex].fileIndex = fileIndex;
        stratumTable[stratumIndex].lineIndex = lineIndex;
        ++stratumIndex;
        currentFileId = 0;
    }

    /**
     * The beginning of a stratum's info
     */
    void stratumSection() {
        storeStratum(readLine());
    }

    void fileSection() {
        ignoreLine();
        while (sdePeek() != '*') {
            fileLine();
        }
    }

    void lineSection() {
        ignoreLine();
        while (sdePeek() != '*') {
            lineLine();
        }
    }

    /**
     * Ignore a section we don't know about.
     */
    void ignoreSection() {
        ignoreLine();
        while (sdePeek() != '*') {
            ignoreLine();
        }
    }

    /**
     * A base "Java" stratum is always available, though
     * it is not in the SourceDebugExtension.
     * Create the base stratum.
     */
    void createJavaStratum() {
        baseStratumIndex = stratumIndex;
        storeStratum(BASE_STRATUM_NAME);
        storeFile(1, jplsFilename, NullString);
        /* JPL line numbers cannot exceed 65535 */
        storeLine(1, 65536, 1, 1, 65536, 1);
        storeStratum("Aux"); /* in case they don't declare */
    }

    /**
     * Decode a SourceDebugExtension which is in SourceMap format.
     * This is the entry point into the recursive descent parser.
     */
    void decode() {
        /* check for "SMAP" - allow EOF if not ours */
        if ((sourceDebugExtension.length() < 4) ||
            (sdeRead() != 'S') ||
            (sdeRead() != 'M') ||
            (sdeRead() != 'A') ||
            (sdeRead() != 'P')) {
            return; /* not our info */
        }
        ignoreLine(); /* flush the rest */
        jplsFilename = readLine();
        defaultStratumId = readLine();
        createJavaStratum();
        while (true) {
            if (sdeRead() != '*') {
                syntax();
            }
            switch (sdeRead()) {
                case 'S':
                    stratumSection();
                    break;
                case 'F':
                    fileSection();
                    break;
                case 'L':
                    lineSection();
                    break;
                case 'E':
                    /* set end points */
                    storeStratum("*terminator*");
                    isValid = true;
                    return;
                default:
                    ignoreSection();
            }
        }
    }

    void createProxyForAbsentSDE() {
        jplsFilename = null;
        defaultStratumId = BASE_STRATUM_NAME;
        defaultStratumIndex = stratumIndex;
        createJavaStratum();
        storeStratum("*terminator*");
    }

    /***************** query functions ***********************/

    private int stiLineTableIndex(int sti, int jplsLine) {
        int i;
        int lineIndexStart;
        int lineIndexEnd;

        lineIndexStart = stratumTable[sti].lineIndex;
        /* one past end */
        lineIndexEnd = stratumTable[sti+1].lineIndex;
        for (i = lineIndexStart; i < lineIndexEnd; ++i) {
            if ((jplsLine >= lineTable[i].jplsStart) &&
                            (jplsLine <= lineTable[i].jplsEnd)) {
                return i;
            }
        }
        return -1;
    }

    private int stiLineNumber(int sti, int lti, int jplsLine) {
        return lineTable[lti].njplsStart +
                (((jplsLine - lineTable[lti].jplsStart) /
                                   lineTable[lti].jplsLineInc));
    }

    private int fileTableIndex(int sti, int fileId) {
        int i;
        int fileIndexStart = stratumTable[sti].fileIndex;
        /* one past end */
        int fileIndexEnd = stratumTable[sti+1].fileIndex;
        for (i = fileIndexStart; i < fileIndexEnd; ++i) {
            if (fileTable[i].fileId == fileId) {
                return i;
            }
        }
        return -1;
    }

    private int stiFileTableIndex(int sti, int lti) {
        return fileTableIndex(sti, lineTable[lti].fileId);
    }

    boolean isValid() {
        return isValid;
    }
}
