/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.netbeans.core.startup;

import java.io.IOException;
import java.util.Stack;
import java.util.Arrays;
import java.util.logging.FileHandler;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import java.util.logging.Logger;
import java.util.logging.XMLFormatter;

/** Logger that will enable the logging of important events during the startup
 * annotated with real time and possibly time differences.
 *
 * @author Petr Nejedly, Jesse Glick
 */
public class StartLog {
    private static final Logger LOG = Logger.getLogger("org.netbeans.log.startup"); // NOI18N
    private static final Stack<String> actions = new Stack<String>();
    private static final Stack<Throwable> places = new Stack<Throwable>();
    private static final boolean DEBUG_NESTING = Boolean.getBoolean("org.netbeans.log.startup.debug"); // NOI18N
    
    private static final String logProp; 
    private static final String logFileProp;
    static final Handler impl;
    
    static {
        try {
            logProp = System.getProperty( "org.netbeans.log.startup" ); // NOI18N
            // NOI18N
            logFileProp = System.getProperty( "org.netbeans.log.startup.logfile" ); // NOI18N

            if(logProp == null)
                impl = new StartImpl();
            else if("print".equals(logProp))
                impl = new PrintImpl();
            else if("tests".equals(logProp))
                impl = new PerformanceTestsImpl();
            else if("xml".equals(logProp)) {
                if (logFileProp == null) {
                    throw new NullPointerException("Specify also 'org.netbeans.log.startup.logfile' property!"); // NOI18N
                }
                FileHandler h = new FileHandler(logFileProp);
                h.setFormatter(new SimplerFormatter());
                impl = h;
            } else
                throw new Error("Unknown org.netbeans.log.startup value [" + logProp + "], it should be (print or tests or xml) !"); // NOI18N
            register();
        } catch (IOException ex) {
            throw new IllegalStateException(ex);
        }
    }
    
    static void register() {
        LOG.setUseParentHandlers(false);
        LOG.addHandler(impl);
        LOG.setLevel(impl.getLevel());
    }
    
    static void unregister() {
        LOG.removeHandler(impl);
    }
    
    /** Start running some interval action.
     * @param action some identifying string
     */
    public static void logStart( String action ) {
        if (willLog()) {
            LOG.log(Level.FINE, "start", action); // NOI18N
            actions.push(action);
            if (DEBUG_NESTING) {
                places.push(new Throwable("logStart called here:")); // NOI18N
            }
        }
    }

    /** Note that something happened, but not an interval.
     * The log will note only the time elapsed since the last interesting event.
     * @param note some identifying string
     */
    public static void logProgress( String note ) {
        if( willLog() ) {
            LOG.log(Level.FINE, "progress", note); // NOI18N
        }
    }

    /** Stop running some interval action.
     * The log will note the time elapsed since the start of the action.
     * Actions <em>must</em> be properly nested.
     * @param action some identifying string
     */
    public static void logEnd( String action ) {
        if (willLog()) {
            String old = actions.empty() ? null : actions.pop();
            Throwable oldplace = DEBUG_NESTING && !places.empty() ? places.pop() : null;
            if (!action.equals(old)) {
                // Error, not ISE; don't want this caught and reported
                // with ErrorManager, for then you get a wierd cycle!
                if (oldplace != null) {
                    oldplace.printStackTrace();
                } else {
                    System.err.println("Either ending too soon, or no info about caller of unmatched start log."); // NOI18N
                    System.err.println("Try running with -J-Dorg.netbeans.log.startup.debug=true"); // NOI18N
                }
                Error e = new Error("StartLog mismatch: ending '" + action + "' but expecting '" + old + "'; rest of stack: " + actions); // NOI18N
                // Print stack trace since you can get strange situations
                // when ErrorManager tries to report it - may need to initialize
                // ErrorManager, etc.
                e.printStackTrace();
                // Presumably you did want to keep on going at this point.
                System.err.flush();
                LOG.setLevel(Level.OFF);
            }
            LOG.log(Level.FINE, "end", action); // NOI18N
        }
    }

    public static boolean willLog() {
        return LOG.isLoggable(Level.FINE);
    }
    
    /** Logs the startup time. The begining is tracked by this class. 
     *  The end is passed as argument.
     */
    public static void logMeasuredStartupTime(long end){
        LOG.log(Level.FINE, "finish", end);
        if("tests".equals(logProp)) {
            impl.flush();
        }            
    }
    
    /** The dummy, no-op implementation */
    private static class StartImpl extends Handler {
        final long zero = System.nanoTime()/1000000;
        
        StartImpl() {}
        void start( String action, long time ) {}
        void progress( String note, long time ) {}
        void end( String action, long time ) {}
        boolean willLog() {
            return false;
        }

        @Override
        public Level getLevel() {
            return willLog() ? Level.FINEST : Level.OFF;
        }


        @Override
        public void publish(LogRecord rec) {
            Object[] args = rec.getParameters();
            String msg = (args.length >= 1 && args[0] instanceof String) ? (String)args[0] : ""; // NOI18N
            long time = System.nanoTime()/1000000;
            if ("start".equals(rec.getMessage())) { // NOI18N
                start(msg, time);
                return;
            }
            if ("end".equals(rec.getMessage())) { // NOI18N
                end(msg, time);
                return;
            }
            if ("progress".equals(rec.getMessage())) { // NOI18N
                progress(msg, time);
                return;
            }
        }

        @Override
        public void flush() {
        }

        @Override
        public final void close() throws SecurityException {
        }
    }

    private static class PrintImpl extends StartImpl {
        PrintImpl() {}
        private Stack<Long> starts = new Stack<Long>();
        long prog;
        private int indent = 0;
        
        @Override
        synchronized void start( String action, long time ) {
            starts.push(time);
            prog=time;
            System.err.println( getIndentString(indent) + "@" + 
                    (time - zero) + " - " + action + " started" // NOI18N
            );
            indent += 2;
        }
        
        @Override
        synchronized void progress( String note, long time ) {
            System.err.println( getIndentString(indent) + "@" + 
                    (time - zero) + " - " + note + " dT=" + (time - prog) // NOI18N
            );
            prog = time;
        }
        
        @Override
        synchronized void end( String action, long time ) {
            indent -= 2;
            long start = starts.pop();
            prog = time;
            System.err.println( getIndentString(indent) + "@" + 
                    (time - zero) + " - " + action + " finished, took " + // NOI18N
                    (time - start) + "ms" // NOI18N
            );
        }
        
        @Override
        boolean willLog() {
            return true;
        }
        
        private char[] spaces = new char[0];
        private String getIndentString( int indent ) {
            if( spaces.length < indent ) {
                spaces = new char[Math.max( spaces.length*2, indent+10 )];
                Arrays.fill( spaces, ' ');
            }
            return new String(spaces,0, indent);
        }
    }

    private static class PerformanceTestsImpl extends StartImpl {
        private StringBuffer logs = new StringBuffer();
        private Stack<Long> starts = new Stack<Long>();
        long prog;
        private int indent = 0;
        
        PerformanceTestsImpl() {}
        
        @Override
        synchronized void start( String action, long time ) {
            starts.push(time);
            prog=time;
            log(getIndentString(indent) + "@" + 
                    (time - zero) + " - " + action + " started" // NOI18N
            );
            indent += 2;
        }
        
        @Override
        synchronized void progress( String note, long time ) {
            log(getIndentString(indent) + "@" + 
                    (time - zero) + " - " + note + " dT=" + (time - prog) // NOI18N
            );
            prog = time;
        }
        
        @Override
        synchronized void end( String action, long time ) {
            indent -= 2;
            long start = starts.pop();
            prog = time;
            log(getIndentString(indent) + "@" + 
                    (time - zero) + " - " + action + " finished, took " + // NOI18N
                    (time - start) + "ms" // NOI18N
            );
        }
        
        @Override
        boolean willLog() {
            return true;
        }
        
        private char[] spaces = new char[0];
        private String getIndentString( int indent ) {
            if( spaces.length < indent ) {
                spaces = new char[Math.max( spaces.length*2, indent+10 )];
                Arrays.fill( spaces, ' ');
            }
            return new String(spaces,0, indent);
        }
        
        synchronized void log(String log){
            logs.append("\n" + log);
        }

        @Override
        public void publish(LogRecord rec) {
            super.publish(rec);
            if ("finish".equals(rec.getMessage())) { // NOI18N
                long end = (Long)rec.getParameters()[0];
                log("IDE starts t = " + Long.toString(zero) + "\nIDE is running t=" + Long.toString(end) + "\n");
            }
        }
        
        @Override
        public synchronized void flush(){
            if(logFileProp!=null){
                try{
                    java.io.File logFile = new java.io.File(logFileProp);
                    java.io.FileWriter writer = new java.io.FileWriter(logFile);
                    writer.write(logs.toString());
                    writer.close();
                }catch (Exception exc){
                    System.err.println("EXCEPTION rises during startup logging:");
                    exc.printStackTrace(System.err);
                }
            } else
                throw new IllegalStateException("You are trying to log startup logs to unexisting file. You have to set property org.netbeans.log.startup.logfile.");
        }

    }

    private static class SimplerFormatter extends XMLFormatter {
        @Override
        public String getHead(Handler h) {
            StringBuilder sb = new StringBuilder();
            sb.append("<?xml version=\"1.0\"");
            sb.append(" encoding='UTF-8'?>\n");
            sb.append("<log>\n");
            return sb.toString();
        }
    }
}
