package traces;

import java.io.File;
import java.io.InputStream;
import java.io.IOException;
import java.util.NoSuchElementException;

/**
 * A utility class to read in Auspex traces.  This will decompress the
 * trace and emit records.  Note that this is carefully written to
 * very efficiently parse the data and produce no garbage in
 * steady-state.  Partially it does this by performing in-place
 * updates to a provided record.
 */
public class Auspex
{
    private static final boolean BENCHMARK = false;
    private static final int BENCH_SECS = 10;

    private static final int BUFSIZE = 1024*16;

    private final Process _zcat;
    private final InputStream _data;

    private final byte[] _line = new byte[BUFSIZE];
    /**
     * The position to read the next byte from in _line.
     */
    private int _rpos = 0;
    /**
     * The position of the end of the current line, just after the
     * newline.
     */
    private int _linelen = 0;
    /**
     * The position just past the last valid byte of _line.
     */
    private int _linelimit = 0;

    /**
     * Read from the specified compressed Auspex trace.
     */
    public Auspex(File file)
        throws IOException
    {
        String[] cmd = new String[] { "zcat", file.getAbsolutePath() };
        _zcat = Runtime.getRuntime().exec(cmd);
        _data = _zcat.getInputStream();
    }

    /**
     * Fill in a record structure with the next record from the trace.
     */
    public void next(Record record)
        throws IOException, NoSuchElementException
    {
        getLine();

        record.timestamp = getDouble();
        assert _line[_rpos-1] == ' ';

        record.type = getType();
        assert _line[_rpos-1] == ' ';

        record.action = getAction();
        assert _line[_rpos-1] == ' ';

        _rpos += 5;
        record.fid = getHexLong();
        assert _line[_rpos-1] == ' ';

        _rpos += 5;
        record.offset = getInt();
        assert _line[_rpos-1] == ' ';

        _rpos += 6;
        record.size = getInt();
        assert _line[_rpos-1] == ' ';

        _rpos += 6;
        record.host = getInt();
        assert _line[_rpos-1] == '\n';

        assert _rpos == _linelen;
    }

    private void getLine()
        throws IOException, NoSuchElementException
    {
        // Start at the end of the previous line
        int start = _linelen;

        while (true) {
            // Is there a line available?
            for (; _linelen < _linelimit; ++_linelen) {
                if (_line[_linelen] == '\n') {
                    ++_linelen;
                    _rpos = start;
                    return;
                }
            }
            // Have we run out of buffer?
            if (_linelimit == BUFSIZE) {
                if (start == 0)
                    badLine("long line");
                // Shift out the old data
                System.arraycopy(_line, start, _line, 0, _linelimit - start);
                _linelimit -= start;
                _linelen -= start;
                start = 0;
            }
            // Fill up the rest of the buffer
            int res = _data.read(_line, _linelimit, BUFSIZE - _linelimit);
            if (res == -1) {
                assert _linelen == _rpos : "EOF with incomplete line";
                throw new NoSuchElementException("EOF");
            }
            _linelimit += res;
        }
    }

    private double getDouble()
    {
        double value = 0;
        while (_line[_rpos] != '.') {
            value = (value*10) + (_line[_rpos] - '0');
            ++_rpos;
        }
        ++_rpos;
        int place = 10;
        while (_line[_rpos] != ' ') {
            value += (double)(_line[_rpos] - '0') / place;
            place *= 10;
            ++_rpos;
        }
        ++_rpos;
        return value;
    }

    private long getHexLong()
    {
        long value = 0;
        while (_line[_rpos] != ' ') {
            value <<= 4;
            if (_line[_rpos] <= '9')
                value += _line[_rpos] - '0';
            else
                value += _line[_rpos] - 'a' + 10;
            ++_rpos;
        }
        ++_rpos;
        return value;
    }

    private int getInt()
    {
        int value = 0;
        while (_line[_rpos] >= '0' && _line[_rpos] <= '9') {
            value = (value*10) + (_line[_rpos] - '0');
            ++_rpos;
        }
        ++_rpos;
        return value;
    }

    private Type getType()
    {
        switch (_line[_rpos]) {
        case 'A':
            _rpos += 5;
            return Type.ATTR;
        case 'B':
            _rpos += 6;
            return Type.BLOCK;
        case 'D':
            switch (_line[_rpos+1]) {
            case 'i':
                _rpos += 4;
                return Type.DIR;
            case 'e':
                _rpos += 7;
                return Type.DELETE;
            default:
                badLine("type field");
            }
        case 'O':
            _rpos += 5;
            return Type.OPEN;
        case 'C':
            _rpos += 6;
            return Type.CLOSE;
        default:
            badLine("type field");
        }
        return null;
    }

    private Action getAction()
    {
        switch (_line[_rpos]) {
        case 'R':
            switch (_line[_rpos+1]) {
            case 'E':
                _rpos += 5;
                return Action.READ;
            case 'W':
                _rpos += 3;
                return Action.RW;
            default:
                badLine("action field");
            }
        case 'W':
            _rpos += 6;
            return Action.WRITE;
        default:
            badLine("action field");
        }
        return null;
    }

    private void badLine(String name)
    {
        String line = new String(_line, 0, _linelen);
        throw new IllegalStateException("Bad " + name + ": " + line);
    }

    public static enum Type
    {
        ATTR, BLOCK, DIR, DELETE, OPEN, CLOSE, EOT;
    }

    public static enum Action
    {
        READ, WRITE, RW;
    }

    public static class Record
    {
        public double timestamp;
        public Type type;
        public Action action;
        public long fid;
        public int offset;
        public int size;
        public int host;

        @Override
        public String toString()
        {
            return String.format("%16f %-6s %-5s %16x %8d %8d %4d",
                                 timestamp, type, action, fid,
                                 offset, size, host);
        }
    }

    public static void main(String[] args)
    {
        try {
            Auspex tr = new Auspex(new File(args[0]));
            long goal = System.currentTimeMillis() + BENCH_SECS*1000;
            int count = 0;

            Record r = new Record();
            while (true) {
                tr.next(r);
                if (!BENCHMARK) {
                    System.out.println(r);
                } else {
                    ++count;
                    if ((count & 0xFFF) == 0) {
                        if (System.currentTimeMillis() >= goal) {
                            System.out.println(count + " in " +
                                               BENCH_SECS + " secs");
                            System.out.println((double)count/BENCH_SECS +
                                               " per second");
                            break;
                        }
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
            System.exit(1);
        } catch (NoSuchElementException e) {
            return;
        }
    }
}
