/*
 * Copyright 2013-2019 Grzegorz Slowikowski (gslowikowski at gmail dot com)
 *
 * 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.google.code.play2.spm;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;

import com.google.code.sbt.compiler.api.SourcePositionMapper;

/**
 * Abstract Play! Framework source position mapper
 *
 * @author Grzegorz Slowikowski
 */
public abstract class AbstractPlay2SourcePositionMapper implements SourcePositionMapper
{
    private String charsetName;

    /**
     * Sets source files character set name (encoding).
     *
     * @param charsetName source files character set name
     */
    @Override
    public void setCharsetName( String charsetName )
    {
        this.charsetName = charsetName;
    }

    /**
     * Reads the content of the file to a string.
     *
     * @param file a file to read the content of
     * @return the content of the file
     * @throws IOException I/O exception when reading from the file
     */
    protected String readFileAsString( File file )
        throws IOException
    {
        FileInputStream is = new FileInputStream( file );
        try
        {
            byte[] buffer = new byte[8192];
            int len = is.read( buffer );
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            while ( len != -1 )
            {
                out.write( buffer, 0, len );
                len = is.read( buffer );
            }
            return charsetName != null ? new String( out.toByteArray(), charsetName ) : new String( out.toByteArray() );
        }
        finally
        {
            is.close();
        }
    }

}


/*
 * Copyright 2013-2019 Grzegorz Slowikowski (gslowikowski at gmail dot com)
 *
 * 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.google.code.play2.plugin;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.maven.cli.event.ExecutionEventLogger;
import org.apache.maven.execution.DefaultMavenExecutionRequest;
import org.apache.maven.execution.DefaultMavenExecutionResult;
import org.apache.maven.execution.MavenExecutionRequest;
import org.apache.maven.execution.MavenExecutionResult;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.lifecycle.LifecycleExecutor;
import org.apache.maven.model.Resource;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugin.PluginExecutionException;
import org.apache.maven.plugin.logging.Log;
import org.apache.maven.project.MavenProject;

import org.codehaus.plexus.PlexusContainer;
import org.codehaus.plexus.util.DirectoryScanner;

import com.google.code.play2.provider.api.AssetCompilationException;
import com.google.code.play2.provider.api.Play2BuildError;
import com.google.code.play2.provider.api.Play2BuildFailure;
import com.google.code.play2.provider.api.Play2Builder;
import com.google.code.play2.provider.api.Play2BuildException;
import com.google.code.play2.provider.api.RoutesCompilationException;
import com.google.code.play2.provider.api.TemplateCompilationException;

import com.google.code.play2.spm.template.Play2TemplateGeneratedSource;
import com.google.code.play2.spm.template.Play2TemplateSourcePositionMapper;

import com.google.code.play2.watcher.api.FileWatchCallback;
import com.google.code.play2.watcher.api.FileWatchException;
import com.google.code.play2.watcher.api.FileWatchService;
import com.google.code.play2.watcher.api.FileWatcher;

import com.google.code.sbt.compiler.api.Analysis;
import com.google.code.sbt.compiler.api.AnalysisProcessor;
import com.google.code.sbt.compiler.api.CompilerException;
import com.google.code.sbt.compiler.api.Compilers;

public class MavenPlay2Builder implements Play2Builder, FileWatchCallback
{
    private List<MavenProject> projects;

    private String sourceEncoding;

    private List<String> goals;

    private List<String> additionalGoals;

    private String assetsPrefix;

    private Log logger;

    private MavenSession session;

    private LifecycleExecutor lifecycleExecutor;

    private PlexusContainer container;

    private File templateCompilationOutputDirectory;

    private AnalysisProcessor sbtAnalysisProcessor;

    private FileWatchService playWatchService;

    private FileWatcher watcher = null; // created after first successful build

    // Flag to force a reload on the next request.
    // This is set if a compile error occurs, and also by the forceReload method on BuildLink, which is called for
    // example when evolutions have been applied.
//    @volatile private var forceReloadNextTime = false
    private boolean forceReloadNextTime = true;
    // Whether any source files have changed since the last request.
//    @volatile private var changed = false
//    private volatile boolean changed = false;
    private /*?volatile*/ Map<String, Long> changedFiles = new HashMap<String, Long>(); //TODO - moze od razu moduły, a nie pliki
    private Object changedFilesLock = new Object();

    private boolean afterFirstSuccessfulBuild = false;
    private Map<MavenProject, Map<String, File>> currentSourceMaps;
    private Map<MavenProject, Long> currentClasspathTimestamps;
    private Map<MavenProject, Set<String>> currentClasspathFilePaths;

    public MavenPlay2Builder( List<MavenProject> projects, String sourceEncoding, List<String> goals,
                              List<String> additionalGoals, String assetsPrefix, Log logger, MavenSession session,
                              LifecycleExecutor lifecycleExecutor, PlexusContainer container,
                              File templateCompilationOutputDirectory, AnalysisProcessor sbtAnalysisProcessor,
                              FileWatchService playWatchService )
    {
        this.projects = projects;
        this.sourceEncoding = sourceEncoding;
        this.goals = goals;
        this.additionalGoals = additionalGoals;
        this.assetsPrefix = assetsPrefix;
        this.logger = logger;
        this.session = session;
        this.lifecycleExecutor = lifecycleExecutor;
        this.container = container;
        this.templateCompilationOutputDirectory = templateCompilationOutputDirectory;
        this.sbtAnalysisProcessor = sbtAnalysisProcessor;
        this.playWatchService = playWatchService;

        currentSourceMaps = new HashMap<MavenProject, Map<String, File>>( projects.size() );
        currentClasspathTimestamps = new HashMap<MavenProject, Long>( projects.size() );
        currentClasspathFilePaths = new HashMap<MavenProject, Set<String>>( projects.size() );
        for ( MavenProject p: projects )
        {
            currentClasspathTimestamps.put( p, Long.valueOf( 0 ) );
            currentClasspathFilePaths.put( p, Collections.<String>emptySet() );
        }
    }

    @Override /* FileWatchCallback */
    public void close() throws IOException
    {
        if ( watcher != null )
        {
            watcher.close();
        }
    }

    @Override /* FileWatchCallback */
    public void onChange( File changedFile )
    {
        String path = changedFile.getAbsolutePath();
        Long currentTimestamp = Long.valueOf( changedFile.lastModified() );
        synchronized ( changedFilesLock )
        {
            Long prevTimestamp = changedFiles.get( path );
            if ( prevTimestamp == null || !prevTimestamp.equals( currentTimestamp ) )
            {
                logger.debug( "\"" + path + "\" file changed" );
                changedFiles.put( path, currentTimestamp );
            }
        }
    }

    @Override /* Play2Builder */
    public void forceReload()
    {
        forceReloadNextTime = true;
    }

    @Override /* Play2Builder */
    public Object[] findSource( String className, Integer line )
    {
        Object[] result = null;
        String topType = className.split( "\\$" )[0];
        for ( Map<String, File> sourceMap: currentSourceMaps.values() )
        {
            File sourceFile = sourceMap.get( topType );
            if ( sourceFile != null )
            {
                result = new Object[] { sourceFile, line };
                if ( sourceFile.getAbsolutePath().startsWith( templateCompilationOutputDirectory.getAbsolutePath() ) )
                {
                    try
                    {
                        Play2TemplateSourcePositionMapper mapper = new Play2TemplateSourcePositionMapper();
                        mapper.setCharsetName( sourceEncoding );
                        Play2TemplateGeneratedSource template = mapper.getGeneratedSource( sourceFile );
                        if ( template != null )
                        {
                            File originalSourceFile = new File( template.getSourceFileName() );
                            Integer originalLine = null;
                            if ( line != null )
                            {
                                originalLine = Integer.valueOf( template.mapLine( line.intValue() ) );
                            }
                            result = new Object[] { originalSourceFile, originalLine };
                        }
                    }
                    catch ( IOException e )
                    {
                        // ignore
                    }
                    catch ( RuntimeException e )
                    {
                        // ignore
                    }
                }
                break;
            }
        }
        return result;
    }

    @Override /* Play2Builder */
    public boolean build() throws Play2BuildFailure, Play2BuildError/*Play2BuildException*/
    {
        Set<String> changedFilePaths = null;
        Map<String, Long> prevChangedFiles = new HashMap<String, Long>();
        synchronized ( changedFilesLock )
        {
            if ( !changedFiles.isEmpty() )
            {
                changedFilePaths = changedFiles.keySet();
                prevChangedFiles = changedFiles;
                changedFiles = new HashMap<String, Long>();
            }
        //TEST - more code inside synchronized block
        }

        if ( !forceReloadNextTime && changedFilePaths == null /*&& afterFirstSuccessfulBuild*/ )
        {
            return false;
        }

        List<MavenProject> projectsToBuild = projects;
        // - !afterFirstSuccessfulBuild => first build or no previous successful builds, build all modules
        // - currentSourceMaps.isEmpty() => first build, build all modules
        // - projects.size() == 1 => one-module project, just build it
        // - else => not the first build in multimodule-project, calculate modules subset to build
        if ( afterFirstSuccessfulBuild /* !currentSourceMaps.isEmpty() *//* currentSourceMap != null */
            && !forceReloadNextTime && projects.size() > 1 )
        {
            projectsToBuild = calculateProjectsToBuild( changedFilePaths );
        }

        MavenExecutionResult result = executeBuild( projectsToBuild, goals );

        boolean shouldReload = forceReloadNextTime;
        forceReloadNextTime = result.hasExceptions();

        if ( !result.hasExceptions() && !additionalGoals.isEmpty() )
        {
            List<MavenProject> onlyMe = Arrays.asList( new MavenProject[] { session.getCurrentProject() } );

            result = executeBuild( onlyMe, additionalGoals );

            forceReloadNextTime = result.hasExceptions();
        }

        if ( result.hasExceptions() )
        {
            synchronized ( changedFilesLock )
            {
                changedFiles.putAll( prevChangedFiles ); // restore previously changed paths, required for next rebuild
            }
            Throwable firstException = result.getExceptions().get( 0 ); // LifecycleExecutionException
            Throwable t = firstException.getCause();
            if ( t instanceof MojoFailureException )
            {
                Throwable cause = t.getCause();
                if ( cause != null )
                {
                    try
                    {
                        Play2BuildException pbe = null;
                        String causeName = cause.getClass().getName();

                        // sbt-compiler exception
                        if ( CompilerException.class.getName().equals( causeName ) )
                        {
                            pbe = getSBTCompilerBuildException( cause );
                        }
                        else if ( AssetCompilationException.class.getName().equals( causeName )
                            || RoutesCompilationException.class.getName().equals( causeName )
                            || TemplateCompilationException.class.getName().equals( causeName ) )
                        {
                            pbe = getPlayBuildException( cause );
                        }

                        if ( pbe != null )
                        {
                            throw new Play2BuildFailure( pbe, sourceEncoding );
                        }
                        throw new Play2BuildError( t.getClass().getSimpleName() + ": " + t.getMessage(), t );
                    }
                    catch ( Play2BuildFailure e )
                    {
                        throw e;
                    }
                    catch ( Play2BuildError e )
                    {
                        throw e;
                    }
                    catch ( Exception e )
                    {
                        throw new Play2BuildError( ".... , check Maven console" );
                    }
                }
                throw new Play2BuildError( t.getClass().getSimpleName() + ": " + t.getMessage(), t );
            }

            if ( t instanceof PluginExecutionException )
            {
                Throwable cause = t.getCause();
                throw new Play2BuildError( cause.getClass().getSimpleName() + ": " + cause.getMessage(), cause );
            }
            throw new Play2BuildError( t.getClass().getSimpleName() + ": " + t.getMessage(), t );
        }

        // no exceptions
        if ( !afterFirstSuccessfulBuild ) // this was first successful build
        {
            afterFirstSuccessfulBuild = true;

            if ( playWatchService != null )
            {
                // Monitor all existing, not generated (inside output directory) source and resource roots
                List<File> monitoredDirectories = new ArrayList<File>();
                for ( MavenProject p: projects )
                {
                    String targetDirectory = p.getBuild().getDirectory();
                    for ( String sourceRoot: p.getCompileSourceRoots() )
                    {
                        if ( !sourceRoot.startsWith( targetDirectory ) && new File( sourceRoot ).isDirectory() )
                        {
                            monitoredDirectories.add( new File( sourceRoot ) );
                        }
                    }
                    for ( Resource resource: p.getResources() )
                    {
                        String resourceRoot = resource.getDirectory();
                        if ( !resourceRoot.startsWith( targetDirectory ) && new File( resourceRoot ).isDirectory() )
                        {
                            monitoredDirectories.add( new File( resourceRoot ) );
                        }
                    }
                }
                //TODO - remove roots nested inside another roots (is it possible?)

                try
                {
                    watcher = playWatchService.watch( monitoredDirectories, this );
                }
                catch ( FileWatchException e )
                {
                    logger.warn( "File watcher initialization failed. Running without hot-reload functionality.", e );
                }
            }
        }

        Map<MavenProject, Map<String, File>> sourceMaps = new HashMap<MavenProject, Map<String, File>>( currentSourceMaps );
        for ( MavenProject p: projectsToBuild )
        {
            Map<String, File> sourceMap = new HashMap<String, File>();
            File classesDirectory = new File( p.getBuild().getOutputDirectory() );
            String classesDirectoryPath = classesDirectory.getAbsolutePath() + File.separator;
            File analysisCacheFile = defaultAnalysisCacheFile( p );
            Analysis analysis = sbtAnalysisProcessor.readFromFile( analysisCacheFile );
            for ( File sourceFile : analysis.getSourceFiles() )
            {
                Set<File> sourceFileProducts = analysis.getProducts( sourceFile );
                for ( File product : sourceFileProducts )
                {
                    String absolutePath = product.getAbsolutePath();
                    if ( absolutePath.contains( "$" ) )
                    {
                        continue; // skip inner and object classes
                    }
                    String relativePath = absolutePath.substring( classesDirectoryPath.length() );
//                    String name = product.getName();
                    String name = relativePath.substring( 0, relativePath.length() - ".class".length() );
                    /*if (name.indexOf( '$' ) > 0)
                    {
                        name = name.substring( 0, name.indexOf( '$' ) );
                    }*/
                    name = name.replace( File.separator, "." );
                    //System.out.println(sourceFile.getPath() + " -> " + name);
                    sourceMap.put( name, sourceFile );
                }
                /*String[] definitionNames = analysis.getDefinitionNames( sourceFile );
                Set<String> uniqueDefinitionNames = new HashSet<String>(definitionNames.length);
                for (String definitionName: definitionNames)
                {
                    if ( !uniqueDefinitionNames.contains( definitionName ) )
                    {
                        result.put( definitionName, sourceFile );
//                        System.out.println( "definitionName:'" + definitionName + "', source:'"
//                                        + sourceFile.getAbsolutePath() + "'" );
                        uniqueDefinitionNames.add( definitionName );
                    }
                }*/
            }
            sourceMaps.put( p, sourceMap );
        }
        this.currentSourceMaps = sourceMaps;

        for ( MavenProject p: projectsToBuild )
        {
            long lastModifiedTime = 0L;
            Set<String> outputFilePaths = new HashSet<String>();
            File outputDirectory = new File( p.getBuild().getOutputDirectory() );
            if ( outputDirectory.exists() && outputDirectory.isDirectory() )
            {
                DirectoryScanner classPathScanner = new DirectoryScanner();
                classPathScanner.setBasedir( outputDirectory );
                classPathScanner.setExcludes( new String[] { assetsPrefix + "**" } );
                classPathScanner.scan();
                String[] files = classPathScanner.getIncludedFiles();
                for ( String fileName: files )
                {
                    File f = new File( outputDirectory, fileName );
                    outputFilePaths.add( f.getAbsolutePath() );
                    long lmf = f.lastModified();
                    if ( lmf > lastModifiedTime )
                    {
                        lastModifiedTime = lmf;
                    }
                }
            }
            if ( !shouldReload
                && ( lastModifiedTime > currentClasspathTimestamps.get( p ).longValue() || !outputFilePaths.equals( currentClasspathFilePaths.get( p ) ) ) )
            {
                shouldReload = true;
            }
            currentClasspathTimestamps.put( p, Long.valueOf( lastModifiedTime ) );
            currentClasspathFilePaths.put( p, outputFilePaths );
        }

        return shouldReload;
    }

    private MavenExecutionResult executeBuild( List<MavenProject> projectsToBuild, List<String> goalsToExecute )
    {
        MavenExecutionRequest request = DefaultMavenExecutionRequest.copy( session.getRequest() );
        request.setStartTime( new Date() );
        request.setExecutionListener( new ExecutionEventLogger() );
        request.setGoals( goalsToExecute );

        MavenExecutionResult result = new DefaultMavenExecutionResult();

        final MavenSession newSession = new MavenSession( container, session.getRepositorySession(), request, result );
        newSession.setProjects( projectsToBuild );
        newSession.setCurrentProject( session.getCurrentProject() );
        newSession.setParallel( session.isParallel() );
        newSession.setProjectDependencyGraph( session.getProjectDependencyGraph() );

        Thread mavenBuildThread = new Thread( new MavenPlay2BuilderRunnable( lifecycleExecutor, newSession ) );
        mavenBuildThread.start();
        try
        {
            mavenBuildThread.join();
        }
        catch ( InterruptedException e )
        {
            throw new RuntimeException ( e );
        }

        return result;
    }

    private Play2BuildException getPlayBuildException( Throwable playException )
    {
        Play2BuildException result = null;
        try
        {
            String message = playException.getClass().getMethod( "getMessage" ).invoke( playException ).toString();
            Object file = playException.getClass().getMethod( "source" ).invoke( playException );
            String fileAbsolutePath = file.getClass().getMethod( "getAbsolutePath" ).invoke( file ).toString();
            Integer line = ( Integer ) playException.getClass().getMethod( "line" ).invoke( playException );
            Integer position = ( Integer ) playException.getClass().getMethod( "position" ).invoke( playException );
            result = new Play2BuildException( new File( fileAbsolutePath ), message, line.intValue(), position.intValue() );
        }
        catch ( IllegalAccessException e )
        {
            // ignore
        }
        catch ( InvocationTargetException e )
        {
            // ignore
        }
        catch ( NoSuchMethodException e )
        {
            // ignore
        }
        catch ( SecurityException e )
        {
            // ignore
        }
        return result;
    }

    private Play2BuildException getSBTCompilerBuildException( Throwable compilerException )
                    throws IllegalAccessException, InvocationTargetException, NoSuchMethodException, SecurityException
    {
        boolean hasGetProblemsMethod = false;
        for ( java.lang.reflect.Method m: compilerException.getClass().getMethods() )
        {
            if ( "getProblems".equals( m.getName() ) )
            {
                hasGetProblemsMethod = true;
                break;
            }
        }
        if ( !hasGetProblemsMethod )
        {
            //TODO - add warning about too old sbt-compiler-maven-plugin version
            return null; //TODO - how to display meaningful message on the screen?
        }

        Play2BuildException result = null;
        Object problems = compilerException.getClass().getMethod( "getProblems" ).invoke( compilerException );
        if ( problems.getClass().isArray() )
        {
            int length = Array.getLength( problems );
            for ( int i = 0; i < length; i ++ )
            {
                Object problem = Array.get( problems, i );
                String severity = problem.getClass().getMethod( "getSeverity"  ).invoke( problem ).toString();
                if ( "Error".equals( severity ) )
                {
                    //String category = problem.getClass().getMethod( "getCategory"  ).invoke( problem ).toString();
                    String message = problem.getClass().getMethod( "getMessage"  ).invoke( problem ).toString();
                    Object sourcePosition = problem.getClass().getMethod( "getPosition"  ).invoke( problem );
                    if ( sourcePosition != null )
                    {
                        Integer line = ( Integer ) sourcePosition.getClass().getMethod( "getLine" ).invoke( sourcePosition );
                        //String lineContent = sourcePosition.getClass().getMethod( "getLineContent" ).invoke( sourcePosition ).toString();
                        //Object offset = sourcePosition.getClass().getMethod( "getOffset" ).invoke( sourcePosition );
                        Integer pointer = ( Integer ) sourcePosition.getClass().getMethod( "getPointer" ).invoke( sourcePosition );
                        Object file = sourcePosition.getClass().getMethod( "getFile" ).invoke( sourcePosition ); //FIXME is it nullable?
                        String fileAbsolutePath = file.getClass().getMethod( "getAbsolutePath" ).invoke( file ).toString();
                        result = new Play2BuildException( new File( fileAbsolutePath ), message, line.intValue(), pointer.intValue() );
                    }
                    else
                    {
                        result = new Play2BuildException( null, message, 0, 0 );
                    }
                    break;
                }
            }
        }
        return result;
    }

    // === TEST ===================================================================================

    private File defaultAnalysisCacheFile( MavenProject p )
    {
        File classesDirectory = new File( p.getBuild().getOutputDirectory() );
        return new File( Compilers.getCacheDirectory( classesDirectory ), "compile" );
    }

    // === TEST ===================================================================================

    private List<MavenProject> calculateProjectsToBuild( Set<String> changedFilePaths )
    {
        Set<MavenProject> changedProjects = new HashSet<MavenProject>( projects.size() );
        for ( String path: changedFilePaths )
        {
            MavenProject p = findProjectFor( path );
            changedProjects.add( p );
        }

        Set<MavenProject> changedAndDependentProjects = new HashSet<MavenProject>( projects.size() );
        for ( MavenProject p: changedProjects )
        {
            changedAndDependentProjects.add( p );
            List<MavenProject> deps = session.getProjectDependencyGraph().getDownstreamProjects( p, true/*transitive*/ );
            for ( MavenProject depP: deps )
            {
                if ( projects.contains( depP ) )
                {
                    changedAndDependentProjects.addAll( deps );
                }
            }
        }

        List<MavenProject> result = new ArrayList<MavenProject>( changedAndDependentProjects.size() );
        for ( MavenProject p: projects )
        {
            if ( changedAndDependentProjects.contains( p ) )
            {
                result.add( p );
            }
        }
        return result;
    }

    private MavenProject findProjectFor( String filePath )
    {
        MavenProject result = null;
        search:
        for ( MavenProject p: projects )
        {
            for ( String sourceRoot: p.getCompileSourceRoots() )
            {
                if ( filePath.startsWith( sourceRoot ) )
                {
                    result = p;
                    break search;
                }
            }
            for ( Resource resource: p.getResources() )
            {
                if ( filePath.startsWith( resource.getDirectory() ) )
                {
                    result = p;
                    break search;
                }
            }
        }
        return result;
    }

}


/*
 * Copyright 2013-2019 Grzegorz Slowikowski (gslowikowski at gmail dot com)
 *
 * 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.google.code.play2.watcher.polling;

import java.io.File;
import java.util.List;

import com.google.code.play2.watcher.api.AbstractFileWatchService;
import com.google.code.play2.watcher.api.FileWatchException;
import com.google.code.play2.watcher.api.FileWatchLogger;
import com.google.code.play2.watcher.api.FileWatcher;
import com.google.code.play2.watcher.api.FileWatchCallback;
import com.google.code.play2.watcher.api.FileWatchService;

import org.codehaus.plexus.component.annotations.Component;

/**
 * Polling file watch service.
 */
@Component( role = FileWatchService.class, hint = "polling", description = "Polling" )
public class PollingFileWatchService
    extends AbstractFileWatchService
{

    /**
     * Default poll interval in milliseconds.
     */
    private final int DEFAULT_POLL_INTERVAL = 1000;

    /**
     * Poll interval in milliseconds.
     */
    private int pollInterval;

    /**
     * Creates polling file watch service.
     */
    public PollingFileWatchService()
    {
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void initialize( FileWatchLogger log ) throws FileWatchException
    {
        super.initialize( log );

        pollInterval = DEFAULT_POLL_INTERVAL;
        String pollIntervalProp = System.getProperty( "play2.pollInterval" );
        if ( pollIntervalProp != null )
        {
            try
            {
                pollInterval = Integer.parseInt( pollIntervalProp );
            }
            catch ( NumberFormatException e )
            {
                log.warn( String.format( "Unparsable property value \"%s\", using default poll interval %d ms",
                                         pollIntervalProp, Integer.valueOf( DEFAULT_POLL_INTERVAL ) ) );
                // throw new FileWatchException( "PollingFileWatcher initialization failed", e );
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public FileWatcher watch( List<File> filesToWatch, FileWatchCallback watchCallback )
    {
        PollingFileWatcher result = new PollingFileWatcher( log, filesToWatch, watchCallback, pollInterval );

        Thread thread = new Thread( result, "polling-play-watch-service" );
        thread.setDaemon( true );
        thread.start();

        return result;
    }

}


/*
 * Copyright 2013-2019 Grzegorz Slowikowski (gslowikowski at gmail dot com)
 *
 * 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.google.code.play2.plugin;

import java.io.File;

import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.BuildLogger;
import org.apache.tools.ant.NoBannerLogger;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.ProjectHelper;
import org.apache.tools.ant.taskdefs.Java;
import org.apache.tools.ant.types.Environment;

/**
 * Base class for Ant Java task using mojos.
 *
 * @author Grzegorz Slowikowski
 */
public abstract class AbstractAntJavaBasedPlay2Mojo
    extends AbstractPlay2Mojo
{

    /**
     * Internal runnable wrapper for Ant Java task.
     */
    protected static class JavaRunnable
        implements Runnable
    {
        private Java java;

        private boolean executed;

        private BuildException exception;

        /**
         * Creates Runnable for given Ant Java task.
         *
         * @param java Java task to be run
         */
        public JavaRunnable( Java java )
        {
            this.java = java;
        }

        /**
         * Returns information if execution has already finished.
         *
         * @return true if execution finished
         */
        public boolean isExecuted()
        {
            boolean result;
            synchronized ( this )
            {
                result = executed;
            }
            return result;
        }

        /**
         * Returns execution exception if it has been thrown.
         *
         * @return exception if it has been thrown
         */
        public BuildException getException()
        {
            BuildException result = null;
            synchronized ( this )
            {
                result = exception;
            }
            return result;
        }

        @Override
        public void run()
        {
            try
            {
                java.execute();
                synchronized ( this )
                {
                    this.executed = true;
                }
            }
            catch ( BuildException e )
            {
                synchronized ( this )
                {
                    this.executed = true;
                    this.exception = e;
                }
            }
        }
    }

    /**
     * Creates and configures Ant project for Java task.
     *
     * @return Ant project for Java task
     */
    protected Project createProject()
    {
        final Project antProject = new Project();

        final ProjectHelper helper = ProjectHelper.getProjectHelper();
        antProject.addReference( ProjectHelper.PROJECTHELPER_REFERENCE, helper );
        helper.getImportStack().addElement( "AntBuilder" ); // import checks that stack is not empty

        final BuildLogger logger = new NoBannerLogger();

        logger.setMessageOutputLevel( Project.MSG_INFO );
        logger.setOutputPrintStream( System.out );
        logger.setErrorPrintStream( System.err );

        antProject.addBuildListener( logger );

        antProject.init();
        antProject.getBaseDir();
        return antProject;
    }

    /**
     * Adds string type system property to Ant Java task.
     *
     * @param java Ant Java task
     * @param propertyName system property name
     * @param propertyValue system property value
     */
    protected void addSystemProperty( Java java, String propertyName, String propertyValue )
    {
        Environment.Variable sysPropPlayHome = new Environment.Variable();
        sysPropPlayHome.setKey( propertyName );
        sysPropPlayHome.setValue( propertyValue );
        java.addSysproperty( sysPropPlayHome );
    }

    /**
     * Adds file type system property to Ant Java task.
     *
     * @param java Ant Java task
     * @param propertyName system property name
     * @param propertyValue system property value
     */
    protected void addSystemProperty( Java java, String propertyName, File propertyValue )
    {
        Environment.Variable sysPropPlayHome = new Environment.Variable();
        sysPropPlayHome.setKey( propertyName );
        sysPropPlayHome.setFile( propertyValue );
        java.addSysproperty( sysPropPlayHome );
    }

}


/*
 * Copyright 2013-2019 Grzegorz Slowikowski (gslowikowski at gmail dot com)
 *
 * 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.google.code.play2.watcher.jnotify;

import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.Field;
import java.util.List;
import java.util.Locale;

import com.google.code.play2.watcher.api.AbstractFileWatchService;
import com.google.code.play2.watcher.api.FileWatchLogger;
import com.google.code.play2.watcher.api.FileWatcher;
import com.google.code.play2.watcher.api.FileWatchCallback;
import com.google.code.play2.watcher.api.FileWatchException;
import com.google.code.play2.watcher.api.FileWatchService;

import net.contentobjects.jnotify.JNotify;
import net.contentobjects.jnotify.JNotifyException;

import org.codehaus.plexus.component.annotations.Component;

/**
 * JNotify file watch service.
 */
@Component( role = FileWatchService.class, hint = "jnotify", description = "JNotify" )
public class JNotifyFileWatchService
    extends AbstractFileWatchService
{
    /**
     * Creates JNotify file watch service.
     */
    public JNotifyFileWatchService()
    {
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void initialize( FileWatchLogger log ) throws FileWatchException
    {
        super.initialize( log );

        File nativeLibsDirectory = new File( "." ); // maybe change to temporary directory?
        String nativeLibsDirectoryProp = System.getProperty( "play2.nativeLibsDirectory" );
        if ( nativeLibsDirectoryProp != null && !"".equals( nativeLibsDirectoryProp ) )
        {
            nativeLibsDirectory = new File( nativeLibsDirectoryProp );
        }
        else
        {
            String targetDirectoryProp = System.getProperty( "project.build.directory" );
            if ( targetDirectoryProp != null && !"".equals( targetDirectoryProp ) )
            {
                File targetDirectory = new File( targetDirectoryProp );
                nativeLibsDirectory = new File( targetDirectory, "native_libraries" );
            }
        }
        String nativeLibsPath = nativeLibsDirectory.getAbsolutePath();

        if ( !nativeLibsDirectory.exists() && !nativeLibsDirectory.mkdirs() )
        {
            throw new FileWatchException( String.format( "Cannot create \"%s\" directory", nativeLibsPath ) );
        }

        String libraryOS = null;
        String libraryName = "jnotify";
        String osName = System.getProperty( "os.name" );
        if ( osName != null )
        {
            osName = osName.toLowerCase( Locale.ENGLISH );
            String architecture = System.getProperty( "sun.arch.data.model" );
            if ( osName.startsWith( "windows" ) )
            {
                libraryOS = "windows" + architecture;
                if ( "amd64".equals( System.getProperty( "os.arch" ) ) )
                {
                    libraryName = "jnotify_64bit";
                }
            }
            else if ( osName.equals( "linux" ) )
            {
                libraryOS = "linux" + architecture;
            }
            else if ( osName.startsWith( "mac os x" ) )
            {
                libraryOS = "osx";
            }
        }

        if ( libraryOS == null )
        {
            throw new FileWatchException(
                                          String.format( "JNotifyFileWatchService initialization failed - unsupported OS \"%s\"",
                                                         osName ) );
        }

        String libraryResourceName = System.mapLibraryName( libraryName );
        libraryResourceName = libraryResourceName.replace( ".dylib", ".jnilib" ); // fix for JDK-7134701 bug
        File outputFile = new File( nativeLibsDirectory, libraryResourceName );
        if ( !outputFile.exists() )
        {
            try
            {
                copyResourceToFile( "META-INF/native/" + libraryOS, libraryResourceName, nativeLibsDirectory );
            }
            catch ( IOException e )
            {
                throw new FileWatchException( "JNotifyFileWatchService initialization failed", e );
            }
        }

        // hack to update java.library.path
        try
        {
            String javaLibraryPath = System.getProperty( "java.library.path" );
            javaLibraryPath =
                javaLibraryPath != null ? javaLibraryPath + File.pathSeparator + nativeLibsPath : nativeLibsPath;
            System.setProperty( "java.library.path", javaLibraryPath );

            Field fieldSysPath = ClassLoader.class.getDeclaredField( "sys_paths" );
            fieldSysPath.setAccessible( true );
            fieldSysPath.set( null, null );
        }
        catch ( Exception e )
        {
            throw new FileWatchException( "JNotifyFileWatchService initialization failed", e );
        }

        // initialize JNotify
        try
        {
            JNotify.removeWatch( 0 );
        }
        catch ( JNotifyException e )
        {
            throw new FileWatchException( "JNotifyFileWatchService initialization failed", e );
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public FileWatcher watch( List<File> filesToWatch, FileWatchCallback watchCallback )
        throws FileWatchException
    {
        try
        {
            return new JNotifyFileWatcher( log, filesToWatch, watchCallback );
        }
        catch ( JNotifyException e )
        {
            throw new FileWatchException( "JNotifyFileWatcher initialization failed", e );
        }
    }

    private void copyResourceToFile( String resourcePath, String libraryName, File outputDirectory )
        throws IOException, FileWatchException
    {
        InputStream is = this.getClass().getClassLoader().getResourceAsStream( resourcePath + "/" + libraryName );
        if ( is == null ) // should never happen
        {
            throw new FileWatchException( "JNotifyFileWatcherService initialization failed, native library resource \""
                + resourcePath + "/" + libraryName + "\" not found" );
        }

        try
        {
            byte[] buffer = new byte[8192];
            int len = is.read( buffer );

            File outputFile = new File( outputDirectory, libraryName );
            OutputStream os = new FileOutputStream( outputFile );
            try
            {
                while ( len != -1 )
                {
                    os.write( buffer, 0, len );
                    len = is.read( buffer );
                }
            }
            finally
            {
                os.flush();
                os.close();
            }
        }
        finally
        {
            is.close();
        }
    }

}


/*
 * Copyright 2013-2019 Grzegorz Slowikowski (gslowikowski at gmail dot com)
 *
 * 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.google.code.play2.spm.template;

import java.io.File;
import java.io.IOException;

import com.google.code.sbt.compiler.api.SourcePosition;
import com.google.code.play2.spm.AbstractPlay2SourcePositionMapper;

/**
 * Play! Framework template source position mapper
 *
 * @author Grzegorz Slowikowski
 */
public class Play2TemplateSourcePositionMapper extends AbstractPlay2SourcePositionMapper
{
    private static final String META_SEPARATOR = "-- GENERATED --";

    /**
     * Performs mapping from the position in generated source file to the position in Twirl template file it was
     * generated from.

     *

     * Returns:
     *

     *
position in Twirl template file

     *
{@code null} value if file of the input position is not recognized as generated from Twirl template file

     *

     *
     * @param p compilation error/warning position in generated file
     * @return position in {@code routes} file or {@code null} value if file of the input position is not recognized as
     *         generated from Twirl template file
     * @throws IOException I/O exception during generated or original source file reading
     */
    @Override
    public SourcePosition map( SourcePosition p ) throws IOException
    {
        SourcePosition result = null;

        File generatedFile = p.getFile();
        int generatedOffset = p.getOffset();
        if ( generatedFile != null && generatedFile.isFile() && generatedOffset >= 0 )
        {
            Play2TemplateGeneratedSource generated = getGeneratedSource( generatedFile );
            if ( generated != null )
            {
                String sourceFileName = generated.getSourceFileName();
                if ( sourceFileName != null )
                {
                    File sourceFile = new File( sourceFileName );
                    if ( sourceFile.isFile() )
                    {
                        int sourceOffset = generated.mapPosition( generatedOffset );
                        String sourceFileContent = readFileAsString( sourceFile );
                        if ( sourceFileContent.length() > sourceOffset )
                        {
                            String[] sourceFileLines = sourceFileContent.split( "\n" );
                            Play2TemplateLocation sourceLocation =
                                new Play2TemplateMapping( sourceFileLines ).location( sourceOffset );
                            //if ( sourceLocation != null )
                            //{
                                result = new Play2TemplateSourcePosition( sourceFile, sourceLocation );
                            //}
                        }
                    }
                }
            }
        }
        return result;
    }

    public Play2TemplateGeneratedSource getGeneratedSource( File generatedFile ) throws IOException
    {
        Play2TemplateGeneratedSource result = null;

        String generatedFileContent = readFileAsString( generatedFile );
        String[] fileSections = generatedFileContent.split( META_SEPARATOR );
        if ( fileSections.length > 1 )
        {
            String[] metaSectionLines = fileSections[1].trim().split( "\n" );
            if ( metaSectionLines.length > 0 )
            {
                result = new Play2TemplateGeneratedSource( metaSectionLines );
            }
        }
        return result;
    }

}


/*
 * Copyright 2013-2019 Grzegorz Slowikowski (gslowikowski at gmail dot com)
 *
 * 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.google.code.play2.plugin;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStreamReader;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.util.Collection;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;

public class LessDependencyCache
{
    private static final String FILE_HEADER = "format: 1";
    private static final String FILE_DEPENDENCY_PREFIX = "  ";

    private Map<String, Set<String>> allDependencies = new TreeMap<String, Set<String>>();

    public void readFromFile( File file )
        throws IOException
    {
        BufferedReader reader = new BufferedReader( new InputStreamReader( new FileInputStream( file ), "UTF-8" ) );
        try
        {
            String line = reader.readLine();
            if ( !FILE_HEADER.equals( line ) )
            {

            }
            //check header
            line = reader.readLine();
            while ( line != null )
            {
                String fileName = line;
                Set<String> dependencies = new TreeSet<String>();
                line = reader.readLine();
                while ( line != null && line.startsWith( FILE_DEPENDENCY_PREFIX ) )
                {
                    String dependencyFileName = line.substring( 2 );
                    dependencies.add( dependencyFileName );
                    line = reader.readLine();
                }
                allDependencies.put( fileName, dependencies );
            }
        }
        finally
        {
            reader.close();
        }
    }

    public void writeToFile( File file )
        throws IOException
    {
        BufferedWriter writer = new BufferedWriter( new OutputStreamWriter( new FileOutputStream( file ), "UTF-8" ) );
        try
        {
            writer.write( FILE_HEADER );
            writer.newLine();
            for ( String fileName: allDependencies.keySet() )
            {
                writer.write( fileName );
                writer.newLine();
                Set<String> deps = allDependencies.get( fileName );
                for ( String dependencyFileName: deps )
                {
                    writer.write( FILE_DEPENDENCY_PREFIX );
                    writer.write( dependencyFileName );
                    writer.newLine();
                }

            }
        }
        finally
        {
            writer.flush();
            writer.close();
        }
    }

    public Set<String> get( String fileName )
    {
        return allDependencies.get( fileName );
    }
//    public Set<String> get( File file )
//    {
//        return allDependencies.get( file.getAbsolutePath() );
//    }

    public void set( String fileName, Collection<String> dependencies )
    {
        allDependencies.put( fileName, new TreeSet<String>( dependencies ) );
    }
//    public void set( File file, Collection<File> dependencies )
//    {
//        Set<String> deps = new TreeSet<String>();
//        for ( File dependencyFile: dependencies )
//        {
//            deps.add( dependencyFile.getAbsolutePath() );
//        }
//        allDependencies.put( file.getAbsolutePath(), deps );
//    }

    @Override
    public int hashCode()
    {
        return allDependencies.hashCode();
    }

    @Override
    public boolean equals( Object obj )
    {
        return obj instanceof LessDependencyCache && allDependencies.equals( ( (LessDependencyCache) obj ).allDependencies );
    }
    //TEMP
    @Override
    public String toString()
    {
        StringBuilder sb = new StringBuilder( '\n' );
        for ( String key: allDependencies.keySet() )
        {
            //sb.append( String.format( "%s\n", key ) );
            sb.append( key );
            sb.append( '\n' );
            Set<String> deps = allDependencies.get( key );
            for ( String dep: deps )
            {
                sb.append( String.format( "  %s\n", dep ) );
            }

        }
        return sb.toString();
    }
}


/**
 * Copyright 2014 Netflix, Inc.
 *
 * 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.netflix.numerus;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicReferenceArray;
import java.util.concurrent.locks.ReentrantLock;

/**
 * A number which can be used to track counters (increment) or set values over time.
 *

 * It is "rolling" in the sense that a 'timeInMilliseconds' is given that you want to track (such as 10 seconds) and then that is broken into buckets (defaults to 10) so that the 10 second window
 * doesn't empty out and restart every 10 seconds, but instead every 1 second you have a new bucket added and one dropped so that 9 of the buckets remain and only the newest starts from scratch.
 *

 * This is done so that the statistics are gathered over a rolling 10 second window with data being added/dropped in 1 second intervals (or whatever granularity is defined by the arguments) rather
 * than each 10 second window starting at 0 again.
 *

 * Performance-wise this class is optimized for writes, not reads. This is done because it expects far higher write volume (thousands/second) than reads (a few per second).
 *

 * For example, on each read to getSum/getCount it will iterate buckets to sum the data so that on writes we don't need to maintain the overall sum and pay the synchronization cost at each write to
 * ensure the sum is up-to-date when the read can easily iterate each bucket to get the sum when it needs it.
 *

 * See UnitTest for usage and expected behavior examples.
 */
public class NumerusRollingNumber {
    private static final Time ACTUAL_TIME = new ActualTime();
    private final Time time;
    final NumerusProperty<Integer> timeInMilliseconds;
    final NumerusProperty<Integer> numberOfBuckets;

    final BucketCircularArray buckets;
    private final CumulativeSum cumulativeSum;
    private final NumerusRollingNumberEvent events;

    public NumerusRollingNumber(NumerusRollingNumberEvent events, NumerusProperty<Integer> timeInMilliseconds, NumerusProperty<Integer> numberOfBuckets) {
        this(events, ACTUAL_TIME, timeInMilliseconds, numberOfBuckets);
    }

    /* used for unit testing */
    /* package for testing */NumerusRollingNumber(NumerusRollingNumberEvent events, Time time, int timeInMilliseconds, int numberOfBuckets) {
        this(events, time, NumerusProperty.Factory.asProperty(timeInMilliseconds), NumerusProperty.Factory.asProperty(numberOfBuckets));
    }

    private NumerusRollingNumber(NumerusRollingNumberEvent events, Time time, NumerusProperty<Integer> timeInMilliseconds, NumerusProperty<Integer> numberOfBuckets) {
        this.events = events;
        this.time = time;
        this.timeInMilliseconds = timeInMilliseconds;
        this.numberOfBuckets = numberOfBuckets;
        this.cumulativeSum = new CumulativeSum(events);

        if (timeInMilliseconds.get() % numberOfBuckets.get() != 0) {
            throw new IllegalArgumentException("The timeInMilliseconds must divide equally into numberOfBuckets. For example 1000/10 is ok, 1000/11 is not.");
        }

        buckets = new BucketCircularArray(numberOfBuckets.get());
    }

    /* package for testing */int getBucketSizeInMilliseconds() {
        return timeInMilliseconds.get() / numberOfBuckets.get();
    }

    /**
     * Increment the counter in the current bucket by one for the given {@link NumerusRollingNumberEvent} type.
     *

     * The {@link NumerusRollingNumberEvent} must be a "counter" type HystrixRollingNumberEvent.isCounter() == true.
     *
     * @param type
     *            HystrixRollingNumberEvent defining which counter to increment
     *            Boolean doNotBlock if currentBucket is null
     */
    public void increment(NumerusRollingNumberEvent type, Boolean doNotBlock) {
        Bucket lastBucket = getCurrentBucket(doNotBlock);
        if (lastBucket == null)
            return;
        lastBucket.getAdder(type).increment();
    }

    /**
     * Increment the counter in the current bucket by one for the given {@link NumerusRollingNumberEvent} type.
     *

     * The {@link NumerusRollingNumberEvent} must be a "counter" type HystrixRollingNumberEvent.isCounter() == true.
     *
     * @param type
     *            HystrixRollingNumberEvent defining which counter to increment
     */
    public void increment(NumerusRollingNumberEvent type) {
        getCurrentBucket().getAdder(type).increment();
    }

    /**
     * Add to the counter in the current bucket for the given {@link NumerusRollingNumberEvent} type.
     *

     * The {@link NumerusRollingNumberEvent} must be a "counter" type HystrixRollingNumberEvent.isCounter() == true.
     *
     * @param type
     *            HystrixRollingNumberEvent defining which counter to add to
     * @param value
     *            long value to be added to the current bucket
     */
    public void add(NumerusRollingNumberEvent type, long value) {
        getCurrentBucket().getAdder(type).add(value);
    }

    /**
     * Update a value and retain the max value.
     *

     * The {@link NumerusRollingNumberEvent} must be a "max updater" type HystrixRollingNumberEvent.isMaxUpdater() == true.
     *
     * @param type
     * @param value
     */
    public void updateRollingMax(NumerusRollingNumberEvent type, long value) {
        getCurrentBucket().getMaxUpdater(type).update(value);
    }

    /**
     * Force a reset of all rolling counters (clear all buckets) so that statistics start being gathered from scratch.
     *

     * This does NOT reset the CumulativeSum values.
     */
    public void reset() {
        // if we are resetting, that means the lastBucket won't have a chance to be captured in CumulativeSum, so let's do it here
        Bucket lastBucket = buckets.peekLast();
        if (lastBucket != null) {
            cumulativeSum.addBucket(lastBucket);
        }

        // clear buckets so we start over again
        buckets.clear();
    }

    /**
     * Get the cumulative sum of all buckets ever since the JVM started without rolling for the given {@link NumerusRollingNumberEvent} type.
     *

     * See {@link #getRollingSum(NumerusRollingNumberEvent)} for the rolling sum.
     *

     * The {@link NumerusRollingNumberEvent} must be a "counter" type HystrixRollingNumberEvent.isCounter() == true.
     *
     * @param type
     * @return cumulative sum of all increments and adds for the given {@link NumerusRollingNumberEvent} counter type
     */
    public long getCumulativeSum(NumerusRollingNumberEvent type) {
        // this isn't 100% atomic since multiple threads can be affecting latestBucket & cumulativeSum independently
        // but that's okay since the count is always a moving target and we're accepting a "point in time" best attempt
        // we are however putting 'getValueOfLatestBucket' first since it can have side-affects on cumulativeSum whereas the inverse is not true
        return getValueOfLatestBucket(type) + cumulativeSum.get(type);
    }

    /**
     * Get the sum of all buckets in the rolling counter for the given {@link NumerusRollingNumberEvent} type.
     *

     * The {@link NumerusRollingNumberEvent} must be a "counter" type HystrixRollingNumberEvent.isCounter() == true.
     *
     * @param type
     *            HystrixRollingNumberEvent defining which counter to retrieve values from
     *            Boolean doNotBlock if currentBucket is null
     * @return
     *         value from the given {@link NumerusRollingNumberEvent} counter type
     */
    public long getRollingSum(NumerusRollingNumberEvent type, Boolean doNotBlock) {
        Bucket lastBucket = getCurrentBucket(doNotBlock);
        if (lastBucket == null)
            return 0;

        long sum = 0;
        for (Bucket b : buckets) {
            sum += b.getAdder(type).sum();
        }
        return sum;
    }

    /**
     * Get the sum of all buckets in the rolling counter for the given {@link NumerusRollingNumberEvent} type.
     *

     * The {@link NumerusRollingNumberEvent} must be a "counter" type HystrixRollingNumberEvent.isCounter() == true.
     *
     * @param type
     *            HystrixRollingNumberEvent defining which counter to retrieve values from
     * @return
     *         value from the given {@link NumerusRollingNumberEvent} counter type
     */
    public long getRollingSum(NumerusRollingNumberEvent type) {
        Bucket lastBucket = getCurrentBucket();
        if (lastBucket == null)
            return 0;

        long sum = 0;
        for (Bucket b : buckets) {
            sum += b.getAdder(type).sum();
        }
        return sum;
    }

    /**
     * Get the value of the latest (current) bucket in the rolling counter for the given {@link NumerusRollingNumberEvent} type.
     *

     * The {@link NumerusRollingNumberEvent} must be a "counter" type HystrixRollingNumberEvent.isCounter() == true.
     *
     * @param type
     *            HystrixRollingNumberEvent defining which counter to retrieve value from
     * @return
     *         value from latest bucket for given {@link NumerusRollingNumberEvent} counter type
     */
    public long getValueOfLatestBucket(NumerusRollingNumberEvent type) {
        Bucket lastBucket = getCurrentBucket();
        if (lastBucket == null)
            return 0;
        // we have bucket data so we'll return the lastBucket
        return lastBucket.get(type);
    }

    /**
     * Get an array of values for all buckets in the rolling counter for the given {@link NumerusRollingNumberEvent} type.
     *

     * Index 0 is the oldest bucket.
     *

     * The {@link NumerusRollingNumberEvent} must be a "counter" type HystrixRollingNumberEvent.isCounter() == true.
     *
     * @param type
     *            HystrixRollingNumberEvent defining which counter to retrieve values from
     * @return array of values from each of the rolling buckets for given {@link NumerusRollingNumberEvent} counter type
     */
    public long[] getValues(NumerusRollingNumberEvent type) {
        Bucket lastBucket = getCurrentBucket();
        if (lastBucket == null)
            return new long[0];

        // get buckets as an array (which is a copy of the current state at this point in time)
        Bucket[] bucketArray = buckets.getArray();

        // we have bucket data so we'll return an array of values for all buckets
        long values[] = new long[bucketArray.length];
        int i = 0;
        for (Bucket bucket : bucketArray) {
            if (type.isCounter()) {
                values[i++] = bucket.getAdder(type).sum();
            } else if (type.isMaxUpdater()) {
                values[i++] = bucket.getMaxUpdater(type).max();
            }
        }
        return values;
    }

    /**
     * Get the max value of values in all buckets for the given {@link NumerusRollingNumberEvent} type.
     *

     * The {@link NumerusRollingNumberEvent} must be a "max updater" type HystrixRollingNumberEvent.isMaxUpdater() == true.
     *
     * @param type
     *            HystrixRollingNumberEvent defining which "max updater" to retrieve values from
     * @return max value for given {@link NumerusRollingNumberEvent} type during rolling window
     */
    public long getRollingMaxValue(NumerusRollingNumberEvent type) {
        long values[] = getValues(type);
        if (values.length == 0) {
            return 0;
        } else {
            Arrays.sort(values);
            return values[values.length - 1];
        }
    }

    private ReentrantLock newBucketLock = new ReentrantLock();

    /* package for testing */Bucket getCurrentBucket() {
       return getCurrentBucket(false);
    }

    /* package for testing */Bucket getCurrentBucket(Boolean doNotBlock) {
        long currentTime = time.getCurrentTimeInMillis();

        /* a shortcut to try and get the most common result of immediately finding the current bucket */

        /**
         * Retrieve the latest bucket if the given time is BEFORE the end of the bucket window, otherwise it returns NULL.
         *
         * NOTE: This is thread-safe because it's accessing 'buckets' which is a LinkedBlockingDeque
         */
        Bucket currentBucket = buckets.peekLast();
        if (currentBucket != null && currentTime < currentBucket.windowStart + getBucketSizeInMilliseconds()) {
            // if we're within the bucket 'window of time' return the current one
            // NOTE: We do not worry if we are BEFORE the window in a weird case of where thread scheduling causes that to occur,
            // we'll just use the latest as long as we're not AFTER the window
            return currentBucket;
        }

        /* if we didn't find the current bucket above, then we have to create one */

        /**
         * The following needs to be synchronized/locked even with a synchronized/thread-safe data structure such as LinkedBlockingDeque because
         * the logic involves multiple steps to check existence, create an object then insert the object. The 'check' or 'insertion' themselves
         * are thread-safe by themselves but not the aggregate algorithm, thus we put this entire block of logic inside synchronized.
         *
         * I am using a tryLock if/then (http://download.oracle.com/javase/6/docs/api/java/util/concurrent/locks/Lock.html#tryLock())
         * so that a single thread will get the lock and as soon as one thread gets the lock all others will go the 'else' block
         * and just return the currentBucket until the newBucket is created. This should allow the throughput to be far higher
         * and only slow down 1 thread instead of blocking all of them in each cycle of creating a new bucket based on some testing
         * (and it makes sense that it should as well).
         *
         * This means the timing won't be exact to the millisecond as to what data ends up in a bucket, but that's acceptable.
         * It's not critical to have exact precision to the millisecond, as long as it's rolling, if we can instead reduce the impact synchronization.
         *
         * More importantly though it means that the 'if' block within the lock needs to be careful about what it changes that can still
         * be accessed concurrently in the 'else' block since we're not completely synchronizing access.
         *
         * For example, we can't have a multi-step process to add a bucket, remove a bucket, then update the sum since the 'else' block of code
         * can retrieve the sum while this is all happening. The trade-off is that we don't maintain the rolling sum and let readers just iterate
         * bucket to calculate the sum themselves. This is an example of favoring write-performance instead of read-performance and how the tryLock
         * versus a synchronized block needs to be accommodated.
         */
        if (newBucketLock.tryLock()) {
            try {
                if (buckets.peekLast() == null) {
                    // the list is empty so create the first bucket
                    Bucket newBucket = new Bucket(events, currentTime);
                    buckets.addLast(newBucket);
                    return newBucket;
                } else {
                    // We go into a loop so that it will create as many buckets as needed to catch up to the current time
                    // as we want the buckets complete even if we don't have transactions during a period of time.
                    for (int i = 0; i < numberOfBuckets.get(); i++) {
                        // we have at least 1 bucket so retrieve it
                        Bucket lastBucket = buckets.peekLast();
                        if (currentTime < lastBucket.windowStart + getBucketSizeInMilliseconds()) {
                            // if we're within the bucket 'window of time' return the current one
                            // NOTE: We do not worry if we are BEFORE the window in a weird case of where thread scheduling causes that to occur,
                            // we'll just use the latest as long as we're not AFTER the window
                            return lastBucket;
                        } else if (currentTime - (lastBucket.windowStart + getBucketSizeInMilliseconds()) > timeInMilliseconds.get()) {
                            // the time passed is greater than the entire rolling counter so we want to clear it all and start from scratch
                            reset();
                            // recursively call getCurrentBucket which will create a new bucket and return it
                            return getCurrentBucket();
                        } else { // we're past the window so we need to create a new bucket
                            // create a new bucket and add it as the new 'last'
                            buckets.addLast(new Bucket(events, lastBucket.windowStart + getBucketSizeInMilliseconds()));
                            // add the lastBucket values to the cumulativeSum
                            cumulativeSum.addBucket(lastBucket);
                        }
                    }
                    // we have finished the for-loop and created all of the buckets, so return the lastBucket now
                    return buckets.peekLast();
                }
            } finally {
                newBucketLock.unlock();
            }
        } else {
            currentBucket = buckets.peekLast();
            if (currentBucket != null) {
                // we didn't get the lock so just return the latest bucket while another thread creates the next one
                return currentBucket;
            } else {
                // the rare scenario where multiple threads raced to create the very first bucket
                // wait slightly and then use recursion while the other thread finishes creating a bucket
                if (doNotBlock) {
                    // caller indicates doNotBlock
                    return null;
                }
                try {
                    Thread.sleep(5);
                } catch (Exception e) {
                    // ignore
                }
                return getCurrentBucket(false);
            }
        }
    }

    /* package */static interface Time {
        public long getCurrentTimeInMillis();
    }

    private static class ActualTime implements Time {

        @Override
        public long getCurrentTimeInMillis() {
            return System.currentTimeMillis();
        }

    }

    /**
     * Counters for a given 'bucket' of time.
     */
    /* package */static class Bucket {
        final long windowStart;
        final LongAdder[] adderForCounterType;
        final LongMaxUpdater[] updaterForCounterType;

        Bucket(NumerusRollingNumberEvent events, long startTime) {
            this.windowStart = startTime;

            /*
             * We support both LongAdder and LongMaxUpdater in a bucket but don't want the memory allocation
             * of all types for each so we only allocate the objects if the HystrixRollingNumberEvent matches
             * the correct type - though we still have the allocation of empty arrays to the given length
             * as we want to keep using the type.ordinal() value for fast random access.
             */

            // initialize the array of LongAdders
            adderForCounterType = new LongAdder[events.getValues().length];
            for (NumerusRollingNumberEvent type : events.getValues()) {
                if (type.isCounter()) {
                    adderForCounterType[type.ordinal()] = new LongAdder();
                }
            }

            updaterForCounterType = new LongMaxUpdater[events.getValues().length];
            for (NumerusRollingNumberEvent type : events.getValues()) {
                if (type.isMaxUpdater()) {
                    updaterForCounterType[type.ordinal()] = new LongMaxUpdater();
                    // initialize to 0 otherwise it is Long.MIN_VALUE
                    updaterForCounterType[type.ordinal()].update(0);
                }
            }
        }

        long get(NumerusRollingNumberEvent type) {
            if (type.isCounter()) {
                return adderForCounterType[type.ordinal()].sum();
            }
            if (type.isMaxUpdater()) {
                return updaterForCounterType[type.ordinal()].max();
            }
            throw new IllegalStateException("Unknown type of event: " + type.name());
        }

        LongAdder getAdder(NumerusRollingNumberEvent type) {
            if (!type.isCounter()) {
                throw new IllegalStateException("Type is not a Counter: " + type.name());
            }
            return adderForCounterType[type.ordinal()];
        }

        LongMaxUpdater getMaxUpdater(NumerusRollingNumberEvent type) {
            if (!type.isMaxUpdater()) {
                throw new IllegalStateException("Type is not a MaxUpdater: " + type.name());
            }
            return updaterForCounterType[type.ordinal()];
        }

    }

    /**
     * Cumulative counters (from start of JVM) from each Type
     */
    /* package */static class CumulativeSum {
        final LongAdder[] adderForCounterType;
        final LongMaxUpdater[] updaterForCounterType;
        final NumerusRollingNumberEvent event;

        CumulativeSum(NumerusRollingNumberEvent event) {
            this.event = event;

            /*
             * We support both LongAdder and LongMaxUpdater in a bucket but don't want the memory allocation
             * of all types for each so we only allocate the objects if the HystrixRollingNumberEvent matches
             * the correct type - though we still have the allocation of empty arrays to the given length
             * as we want to keep using the type.ordinal() value for fast random access.
             */

            // initialize the array of LongAdders
            adderForCounterType = new LongAdder[event.getValues().length];
            for (NumerusRollingNumberEvent type : event.getValues()) {
                if (type.isCounter()) {
                    adderForCounterType[type.ordinal()] = new LongAdder();
                }
            }

            updaterForCounterType = new LongMaxUpdater[event.getValues().length];
            for (NumerusRollingNumberEvent type : event.getValues()) {
                if (type.isMaxUpdater()) {
                    updaterForCounterType[type.ordinal()] = new LongMaxUpdater();
                    // initialize to 0 otherwise it is Long.MIN_VALUE
                    updaterForCounterType[type.ordinal()].update(0);
                }
            }
        }

        public void addBucket(Bucket lastBucket) {
            for (NumerusRollingNumberEvent type : event.getValues()) {
                if (type.isCounter()) {
                    getAdder(type).add(lastBucket.getAdder(type).sum());
                }
                if (type.isMaxUpdater()) {
                    getMaxUpdater(type).update(lastBucket.getMaxUpdater(type).max());
                }
            }
        }

        long get(NumerusRollingNumberEvent type) {
            if (type.isCounter()) {
                return adderForCounterType[type.ordinal()].sum();
            }
            if (type.isMaxUpdater()) {
                return updaterForCounterType[type.ordinal()].max();
            }
            throw new IllegalStateException("Unknown type of event: " + type.name());
        }

        LongAdder getAdder(NumerusRollingNumberEvent type) {
            if (!type.isCounter()) {
                throw new IllegalStateException("Type is not a Counter: " + type.name());
            }
            return adderForCounterType[type.ordinal()];
        }

        LongMaxUpdater getMaxUpdater(NumerusRollingNumberEvent type) {
            if (!type.isMaxUpdater()) {
                throw new IllegalStateException("Type is not a MaxUpdater: " + type.name());
            }
            return updaterForCounterType[type.ordinal()];
        }

    }

    /**
     * This is a circular array acting as a FIFO queue.
     *

     * It purposefully does NOT implement Deque or some other Collection interface as it only implements functionality necessary for this RollingNumber use case.
     *

     * Important Thread-Safety Note: This is ONLY thread-safe within the context of RollingNumber and the protection it gives in the getCurrentBucket method. It uses AtomicReference
     * objects to ensure anything done outside of getCurrentBucket is thread-safe, and to ensure visibility of changes across threads (ie. volatility) but the addLast and removeFirst
     * methods are NOT thread-safe for external access they depend upon the lock.tryLock() protection in getCurrentBucket which ensures only a single thread will access them at at time.
     *

     * benjchristensen => This implementation was chosen based on performance testing I did and documented at: http://benjchristensen.com/2011/10/08/atomiccirculararray/
     */
    /* package */class BucketCircularArray implements Iterable<Bucket> {
        private final AtomicReference<ListState> state;
        private final int dataLength; // we don't resize, we always stay the same, so remember this
        private final int numBuckets;

        /**
         * Immutable object that is atomically set every time the state of the BucketCircularArray changes
         *

         * This handles the compound operations
         */
        private class ListState {
            /*
             * this is an AtomicReferenceArray and not a normal Array because we're copying the reference
             * between ListState objects and multiple threads could maintain references across these
             * compound operations so I want the visibility/concurrency guarantees
             */
            private final AtomicReferenceArray<Bucket> data;
            private final int size;
            private final int tail;
            private final int head;

            private ListState(AtomicReferenceArray<Bucket> data, int head, int tail) {
                this.head = head;
                this.tail = tail;
                if (head == 0 && tail == 0) {
                    size = 0;
                } else {
                    this.size = (tail + dataLength - head) % dataLength;
                }
                this.data = data;
            }

            public Bucket tail() {
                if (size == 0) {
                    return null;
                } else {
                    // we want to get the last item, so size()-1
                    return data.get(convert(size - 1));
                }
            }

            private Bucket[] getArray() {
                /*
                 * this isn't technically thread-safe since it requires multiple reads on something that can change
                 * but since we never clear the data directly, only increment/decrement head/tail we would never get a NULL
                 * just potentially return stale data which we are okay with doing
                 */
                ArrayList<Bucket> array = new ArrayList<Bucket>();
                for (int i = 0; i < size; i++) {
                    array.add(data.get(convert(i)));
                }
                return array.toArray(new Bucket[array.size()]);
            }

            private ListState incrementTail() {
                /* if incrementing results in growing larger than 'length' which is the max we should be at, then also increment head (equivalent of removeFirst but done atomically) */
                if (size == numBuckets) {
                    // increment tail and head
                    return new ListState(data, (head + 1) % dataLength, (tail + 1) % dataLength);
                } else {
                    // increment only tail
                    return new ListState(data, head, (tail + 1) % dataLength);
                }
            }

            public ListState clear() {
                return new ListState(new AtomicReferenceArray<Bucket>(dataLength), 0, 0);
            }

            public ListState addBucket(Bucket b) {
                /*
                 * We could in theory have 2 threads addBucket concurrently and this compound operation would interleave.
                 *

                 * This should NOT happen since getCurrentBucket is supposed to be executed by a single thread.
                 *

                 * If it does happen, it's not a huge deal as incrementTail() will be protected by compareAndSet and one of the two addBucket calls will succeed with one of the Buckets.
                 *

                 * In either case, a single Bucket will be returned as "last" and data loss should not occur and everything keeps in sync for head/tail.
                 *

                 * Also, it's fine to set it before incrementTail because nothing else should be referencing that index position until incrementTail occurs.
                 */
                data.set(tail, b);
                return incrementTail();
            }

            // The convert() method takes a logical index (as if head was
            // always 0) and calculates the index within elementData
            private int convert(int index) {
                return (index + head) % dataLength;
            }
        }

        BucketCircularArray(int size) {
            AtomicReferenceArray<Bucket> _buckets = new AtomicReferenceArray<Bucket>(size + 1); // + 1 as extra room for the add/remove;
            state = new AtomicReference<ListState>(new ListState(_buckets, 0, 0));
            dataLength = _buckets.length();
            numBuckets = size;
        }

        public void clear() {
            while (true) {
                /*
                 * it should be very hard to not succeed the first pass thru since this is typically is only called from
                 * a single thread protected by a tryLock, but there is at least 1 other place (at time of writing this comment)
                 * where reset can be called from (CircuitBreaker.markSuccess after circuit was tripped) so it can
                 * in an edge-case conflict.
                 *
                 * Instead of trying to determine if someone already successfully called clear() and we should skip
                 * we will have both calls reset the circuit, even if that means losing data added in between the two
                 * depending on thread scheduling.
                 *
                 * The rare scenario in which that would occur, we'll accept the possible data loss while clearing it
                 * since the code has stated its desire to clear() anyways.
                 */
                ListState current = state.get();
                ListState newState = current.clear();
                if (state.compareAndSet(current, newState)) {
                    return;
                }
            }
        }

        /**
         * Returns an iterator on a copy of the internal array so that the iterator won't fail by buckets being added/removed concurrently.
         */
        public Iterator<Bucket> iterator() {
            return Collections.unmodifiableList(Arrays.asList(getArray())).iterator();
        }

        public void addLast(Bucket o) {
            ListState currentState = state.get();
            // create new version of state (what we want it to become)
            ListState newState = currentState.addBucket(o);

            /*
             * use compareAndSet to set in case multiple threads are attempting (which shouldn't be the case because since addLast will ONLY be called by a single thread at a time due to protection
             * provided in getCurrentBucket)
             */
            if (state.compareAndSet(currentState, newState)) {
                // we succeeded
                return;
            } else {
                // we failed, someone else was adding or removing
                // instead of trying again and risking multiple addLast concurrently (which shouldn't be the case)
                // we'll just return and let the other thread 'win' and if the timing is off the next call to getCurrentBucket will fix things
                return;
            }
        }

        public Bucket getLast() {
            return peekLast();
        }

        public int size() {
            // the size can also be worked out each time as:
            // return (tail + data.length() - head) % data.length();
            return state.get().size;
        }

        public Bucket peekLast() {
            return state.get().tail();
        }

        private Bucket[] getArray() {
            return state.get().getArray();
        }

    }

}