package org.apache.maven.scm.provider.accurev.command.changelog;

/*
 * 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.
 */

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;

import org.apache.maven.scm.ChangeFile;
import org.apache.maven.scm.ChangeSet;
import org.apache.maven.scm.CommandParameter;
import org.apache.maven.scm.CommandParameters;
import org.apache.maven.scm.ScmBranch;
import org.apache.maven.scm.ScmException;
import org.apache.maven.scm.ScmFileSet;
import org.apache.maven.scm.ScmResult;
import org.apache.maven.scm.ScmRevision;
import org.apache.maven.scm.ScmVersion;
import org.apache.maven.scm.command.changelog.ChangeLogScmResult;
import org.apache.maven.scm.command.changelog.ChangeLogSet;
import org.apache.maven.scm.log.ScmLogger;
import org.apache.maven.scm.provider.ScmProviderRepository;
import org.apache.maven.scm.provider.accurev.AccuRev;
import org.apache.maven.scm.provider.accurev.AccuRevCapability;
import org.apache.maven.scm.provider.accurev.AccuRevException;
import org.apache.maven.scm.provider.accurev.AccuRevScmProviderRepository;
import org.apache.maven.scm.provider.accurev.AccuRevVersion;
import org.apache.maven.scm.provider.accurev.FileDifference;
import org.apache.maven.scm.provider.accurev.Stream;
import org.apache.maven.scm.provider.accurev.Transaction;
import org.apache.maven.scm.provider.accurev.Transaction.Version;
import org.apache.maven.scm.provider.accurev.command.AbstractAccuRevCommand;
import org.codehaus.plexus.util.StringUtils;

/**
 * TODO filter results based on project_path Find appropriate start and end transaction ids from parameters. Streams
 * must be the same. Diff on stream start to end - these are the upstream changes Hist on the stream start+1 to end
 * remove items from the upstream set if they appear in the history For workspaces diff doesn't work. So we would not
 * pickup any upstream changes, just the "keep" transactions which is not very useful. Hist on the workspace Then diff /
 * hist on the basis stream, skipping any transactions that are coming from the workspace.
 * 
 * @author ggardner
 */
public class AccuRevChangeLogCommand
    extends AbstractAccuRevCommand
{

    public AccuRevChangeLogCommand( ScmLogger logger )
    {
        super( logger );
    }

    @Override
    protected ScmResult executeAccurevCommand( AccuRevScmProviderRepository repository, ScmFileSet fileSet,
                                               CommandParameters parameters )
        throws ScmException, AccuRevException
    {

        // Do we have a supplied branch. If not we default to the URL stream.
        ScmBranch branch = (ScmBranch) parameters.getScmVersion( CommandParameter.BRANCH, null );
        AccuRevVersion branchVersion = repository.getAccuRevVersion( branch );
        String stream = branchVersion.getBasisStream();
        String fromSpec = branchVersion.getTimeSpec();
        String toSpec = "highest";

        // Versions
        ScmVersion startVersion = parameters.getScmVersion( CommandParameter.START_SCM_VERSION, null );
        ScmVersion endVersion = parameters.getScmVersion( CommandParameter.END_SCM_VERSION, null );

        if ( startVersion != null && StringUtils.isNotEmpty( startVersion.getName() ) )
        {
            AccuRevVersion fromVersion = repository.getAccuRevVersion( startVersion );
            // if no end version supplied then use same basis as startVersion
            AccuRevVersion toVersion =
                endVersion == null ? new AccuRevVersion( fromVersion.getBasisStream(), "now" )
                                : repository.getAccuRevVersion( endVersion );
                
            if ( !Objects.equals( fromVersion.getBasisStream(), toVersion.getBasisStream() ) )
            {
                throw new AccuRevException( "Not able to provide change log between different streams " + fromVersion
                    + "," + toVersion );
            }

            stream = fromVersion.getBasisStream();
            fromSpec = fromVersion.getTimeSpec();
            toSpec = toVersion.getTimeSpec();

        }

        Date startDate = parameters.getDate( CommandParameter.START_DATE, null );
        Date endDate = parameters.getDate( CommandParameter.END_DATE, null );
        int numDays = parameters.getInt( CommandParameter.NUM_DAYS, 0 );

        if ( numDays > 0 )
        {
            if ( ( startDate != null || endDate != null ) )
            {
                throw new ScmException( "Start or end date cannot be set if num days is set." );
            }
            // Last x days.
            int day = 24 * 60 * 60 * 1000;
            startDate = new Date( System.currentTimeMillis() - (long) numDays * day );
            endDate = new Date( System.currentTimeMillis() + day );
        }

        if ( endDate != null && startDate == null )
        {
            throw new ScmException( "The end date is set but the start date isn't." );
        }

        // Date parameters override transaction ids in versions
        if ( startDate != null )
        {
            fromSpec = AccuRevScmProviderRepository.formatTimeSpec( startDate );
        }
        else if ( fromSpec == null )
        {
            fromSpec = "1";
        }

        // Convert the fromSpec to both a date AND a transaction id by looking up
        // the nearest transaction in the depot.
        Transaction fromTransaction = getDepotTransaction( repository, stream, fromSpec );

        long fromTranId = 1;
        if ( fromTransaction != null )
        {
            // This tran id is less than or equal to the date/tranid we requested.
            fromTranId = fromTransaction.getTranId();
            if ( startDate == null )
            {
                startDate = fromTransaction.getWhen();
            }
        }

        if ( endDate != null )
        {
            toSpec = AccuRevScmProviderRepository.formatTimeSpec( endDate );
        }
        else if ( toSpec == null )
        {
            toSpec = "highest";
        }

        Transaction toTransaction = getDepotTransaction( repository, stream, toSpec );
        long toTranId = 1;
        if ( toTransaction != null )
        {
            toTranId = toTransaction.getTranId();
            if ( endDate == null )
            {
                endDate = toTransaction.getWhen();
            }
        }
        startVersion = new ScmRevision( repository.getRevision( stream, fromTranId ) );
        endVersion = new ScmRevision( repository.getRevision( stream, toTranId ) );

        //TODO Split this method in two here. above to convert params to start and end (stream,tranid,date) and test independantly
        
        List<Transaction> streamHistory = Collections.emptyList();
        List<Transaction> workspaceHistory = Collections.emptyList();
        List<FileDifference> streamDifferences = Collections.emptyList();

        StringBuilder errorMessage = new StringBuilder();

        AccuRev accurev = repository.getAccuRev();

        Stream changelogStream = accurev.showStream( stream );
        if ( changelogStream == null )
        {
            errorMessage.append( "Unknown accurev stream -" ).append( stream ).append( "." );
        }
        else
        {

            String message =
                "Changelog on stream " + stream + "(" + changelogStream.getStreamType() + ") from " + fromTranId + " ("
                    + startDate + "), to " + toTranId + " (" + endDate + ")";

            if ( startDate != null && startDate.after( endDate ) || fromTranId >= toTranId )
            {
                getLogger().warn( "Skipping out of range " + message );
            }
            else
            {

                getLogger().info( message );

                // In 4.7.2 and higher we have a diff command that will list all the file differences in a stream
                // and thus can be used to detect upstream changes
                // Unfortunately diff -v -V -t does not work in workspaces.
                Stream diffStream = changelogStream;
                if ( changelogStream.isWorkspace() )
                {

                    workspaceHistory =
                        accurev.history( stream, Long.toString( fromTranId + 1 ), Long.toString( toTranId ), 0, false,
                                         false );

                    if ( workspaceHistory == null )
                    {
                        errorMessage.append( "history on workspace " + stream + " from " + fromTranId + 1 + " to "
                            + toTranId + " failed." );

                    }

                    // do the diff/hist on the basis stream instead.
                    stream = changelogStream.getBasis();
                    diffStream = accurev.showStream( stream );

                }

                if ( AccuRevCapability.DIFF_BETWEEN_STREAMS.isSupported( accurev.getClientVersion() ) )
                {
                    if ( startDate.before( diffStream.getStartDate() ) )
                    {
                        getLogger().warn( "Skipping diff of " + stream + " due to start date out of range" );
                    }
                    else
                    {
                        streamDifferences =
                            accurev.diff( stream, Long.toString( fromTranId ), Long.toString( toTranId ) );
                        if ( streamDifferences == null )
                        {
                            errorMessage.append( "Diff " + stream + "- " + fromTranId + " to " + toTranId + "failed." );
                        }
                    }
                }

                // History needs to start from the transaction after our starting transaction

                streamHistory =
                    accurev.history( stream, Long.toString( fromTranId + 1 ), Long.toString( toTranId ), 0, false,
                                     false );
                if ( streamHistory == null )
                {
                    errorMessage.append( "history on stream " + stream + " from " + fromTranId + 1 + " to " + toTranId
                        + " failed." );
                }

            }
        }

        String errorString = errorMessage.toString();
        if ( StringUtils.isBlank( errorString ) )
        {
            ChangeLogSet changeLog =
                getChangeLog( changelogStream, streamDifferences, streamHistory, workspaceHistory, startDate, endDate );

            changeLog.setEndVersion( endVersion );
            changeLog.setStartVersion( startVersion );

            return new ChangeLogScmResult( accurev.getCommandLines(), changeLog );
        }
        else
        {
            return new ChangeLogScmResult( accurev.getCommandLines(), "AccuRev errors: " + errorMessage,
                                           accurev.getErrorOutput(), false );
        }

    }

    private Transaction getDepotTransaction( AccuRevScmProviderRepository repo, String stream, String tranSpec )
        throws AccuRevException
    {
        return repo.getDepotTransaction( stream, tranSpec );
    }

    private ChangeLogSet getChangeLog( Stream stream, List<FileDifference> streamDifferences,
                                       List<Transaction> streamHistory, List<Transaction> workspaceHistory,
                                       Date startDate, Date endDate )
    {

        // Collect all the "to" versions from the streamDifferences into a Map by element id
        // If that version is seen in the promote/keep history then we move it from the map
        // At the end we create a pseudo ChangeSet for any remaining entries in the map as
        // representing "upstream changes"
        Map<Long, FileDifference> differencesMap = new HashMap<Long, FileDifference>();
        for ( FileDifference fileDifference : streamDifferences )
        {
            differencesMap.put( fileDifference.getElementId(), fileDifference );
        }

        List<Transaction> mergedHistory = new ArrayList<Transaction>( streamHistory );
        // will never match a version
        String streamPrefix = "/";

        mergedHistory.addAll( workspaceHistory );
        streamPrefix = stream.getId() + "/";

        List<ChangeSet> entries = new ArrayList<ChangeSet>( streamHistory.size() );
        for ( Transaction t : mergedHistory )
        {
            if ( ( startDate != null && t.getWhen().before( startDate ) )
                || ( endDate != null && t.getWhen().after( endDate ) ) )
            {
                // This is possible if dates and transactions are mixed in the time spec.
                continue;
            }

            // Needed to make Tck test pass against accurev > 4.7.2 - the changelog only expects to deal with
            // files. Stream changes and cross links are important entries in the changelog.
            // However we should only see mkstream once and it is irrelevant given we are interrogating
            // the history of this stream.
            if ( "mkstream".equals( t.getTranType() ) )
            {
                continue;
            }

            Collection<Version> versions = t.getVersions();
            List<ChangeFile> files = new ArrayList<ChangeFile>( versions.size() );

            for ( Version v : versions )
            {

                // Remove diff representing this promote
                FileDifference difference = differencesMap.get( v.getElementId() );
                // TODO: how are defuncts shown in the version history?
                if ( difference != null )
                {
                    String newVersionSpec = difference.getNewVersionSpec();
                    if ( newVersionSpec != null && newVersionSpec.equals( v.getRealSpec() ) )
                    {
                        if ( getLogger().isDebugEnabled() )
                        {
                            getLogger().debug( "Removing difference for " + v );
                        }
                        differencesMap.remove( v.getElementId() );
                    }
                }

                // Add this file, unless the virtual version indicates this is the basis stream, and the real
                // version came from our workspace stream (ie, this transaction is a promote from the workspace
                // to its basis stream, and is therefore NOT a change
                if ( v.getRealSpec().startsWith( streamPrefix ) && !v.getVirtualSpec().startsWith( streamPrefix ) )
                {
                    if ( getLogger().isDebugEnabled() )
                    {
                        getLogger().debug( "Skipping workspace to basis stream promote " + v );
                    }
                }
                else
                {
                    ChangeFile f =
                        new ChangeFile( v.getElementName(), v.getVirtualSpec() + " (" + v.getRealSpec() + ")" );
                    files.add( f );
                }

            }

            if ( versions.isEmpty() || !files.isEmpty() )
            {
                ChangeSet changeSet = new ChangeSet( t.getWhen(), t.getComment(), t.getAuthor(), files );

                entries.add( changeSet );
            }
            else
            {
                if ( getLogger().isDebugEnabled() )
                {
                    getLogger().debug( "All versions removed for " + t );
                }
            }

        }

        // Anything left in the differencesMap represents a change from a higher stream
        // We don't have details on who or where these came from, but it is important to
        // detect these for CI tools like Continuum
        if ( !differencesMap.isEmpty() )
        {
            List<ChangeFile> upstreamFiles = new ArrayList<ChangeFile>();
            for ( FileDifference difference : differencesMap.values() )
            {
                if ( difference.getNewVersionSpec() != null )
                {
                    upstreamFiles.add( new ChangeFile( difference.getNewFile().getPath(),
                                                       difference.getNewVersionSpec() ) );
                }
                else
                {
                    // difference is a deletion
                    upstreamFiles.add( new ChangeFile( difference.getOldFile().getPath(), null ) );
                }
            }
            entries.add( new ChangeSet( endDate, "Upstream changes", "various", upstreamFiles ) );
        }

        return new ChangeLogSet( entries, startDate, endDate );
    }

    public ChangeLogScmResult changelog( ScmProviderRepository repo, ScmFileSet testFileSet, CommandParameters params )
        throws ScmException
    {
        return (ChangeLogScmResult) execute( repo, testFileSet, params );
    }

}
