package org.apache.maven.plugins.enforcer;

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

import org.apache.maven.enforcer.rule.api.EnforcerRule;
import org.apache.maven.enforcer.rule.api.EnforcerRuleException;
import org.apache.maven.enforcer.rule.api.EnforcerRuleHelper;
import org.apache.maven.model.Activation;
import org.apache.maven.model.ActivationOS;
import org.apache.maven.model.Profile;
import org.apache.maven.plugin.logging.Log;
import org.apache.maven.model.profile.activation.ProfileActivator;
import org.codehaus.plexus.component.repository.exception.ComponentLookupException;
import org.codehaus.plexus.util.Os;
import org.codehaus.plexus.util.StringUtils;

/**
 * This rule checks that the OS is allowed by combinations of family, name, version and cpu architecture. The behavior
 * is exactly the same as the Maven Os profile activation so the same values are allowed here.
 *
 * @author <a href="mailto:brianf@apache.org">Brian Fox</a>
 */
public class RequireOS
    extends AbstractStandardEnforcerRule
{
    private ProfileActivator activator;

    /**
     * The OS family type desired<br />
     * Possible values:
     * <ul>
     * <li>dos</li>
     * <li>mac</li>
     * <li>netware</li>
     * <li>os/2</li>
     * <li>tandem</li>
     * <li>unix</li>
     * <li>windows</li>
     * <li>win9x</li>
     * <li>z/os</li>
     * <li>os/400</li>
     * </ul>
     * 
     * @see {@link #setFamily(String)}
     * @see {@link #getFamily()}
     */
    private String family = null;

    /**
     * The OS name desired.
     *
     * @see {@link #setName(String)}
     * @see {@link #getName()}
     */
    private String name = null;

    /**
     * The OS version desired.
     * 
     * @see {@link #setVersion(String)}
     * @see {@link #getVersion()}
     */
    private String version = null;

    /**
     * The OS architecture desired.
     * 
     * @see {@link #setArch(String)}
     * @see {@link #getArch()}
     */
    private String arch = null;

    /**
     * Display detected OS information.
     * 
     * @see {@link #setDisplay(boolean)}
     * @see {@link #isDisplay()}
     */
    private boolean display = false;

    /**
     * Instantiates a new RequireOS.
     */
    public RequireOS()
    {

    }
    
    // For testing
    RequireOS( ProfileActivator activator ) 
    {
        this.activator = activator;
    }
    

    @Override
    public void execute( EnforcerRuleHelper helper )
        throws EnforcerRuleException
    {

        displayOSInfo( helper.getLog(), display );

        if ( allParamsEmpty() )
        {
            throw new EnforcerRuleException( "All parameters can not be empty. "
                + "You must pick at least one of (family, name, version, arch) "
                + "or use -Denforcer.os.display=true to see the current OS information." );
        }
        
        try
        {
            activator = helper.getComponent( ProfileActivator.class, "os" );
        }
        catch ( ComponentLookupException e )
        {
            throw new EnforcerRuleException( e.getMessage() );
        }

        if ( isValidFamily( this.family ) )
        {
            if ( !isAllowed() )
            {
                String message = getMessage();
                if ( StringUtils.isEmpty( message ) )
                {
                    //@formatter:off
                    message =
                        ( "OS Arch: " 
                            + Os.OS_ARCH + " Family: " 
                            + Os.OS_FAMILY + " Name: " 
                            + Os.OS_NAME + " Version: "
                            + Os.OS_VERSION + " is not allowed by" + ( arch != null ? " Arch=" + arch : "" )
                            + ( family != null ? " Family=" + family : "" ) 
                            + ( name != null ? " Name=" + name : "" ) 
                            + ( version != null ? " Version=" + version : "" ) );
                    //@formatter:on
                }
                throw new EnforcerRuleException( message );
            }
        }
        else
        {
            final int minimumBufferSize = 50;
            StringBuilder buffer = new StringBuilder( minimumBufferSize );
            Iterator<?> iter = Os.getValidFamilies().iterator();
            while ( iter.hasNext() )
            {
                buffer.append( iter.next() );
                buffer.append( ", " );
            }
            String help = StringUtils.stripEnd( buffer.toString().trim(), "." );
            throw new EnforcerRuleException( "Invalid Family type used. Valid family types are: " + help );
        }
    }

    /**
     * Log the current OS information.
     *
     * @param log the log
     * @param info the info
     */
    public void displayOSInfo( Log log, boolean info )
    {
        String string =
            "OS Info: Arch: " + Os.OS_ARCH + " Family: " + Os.OS_FAMILY + " Name: " + Os.OS_NAME + " Version: "
                + Os.OS_VERSION;

        if ( !info )
        {
            log.debug( string );
        }
        else
        {
            log.info( string );
        }
    }

    /**
     * Helper method to determine if the current OS is allowed based on the injected values for family, name, version
     * and arch.
     *
     * @return true if the version is allowed.
     */
    public boolean isAllowed()
    {
        return activator.isActive( createProfile(), null, null );
    }

    /**
     * Helper method to check that at least one of family, name, version or arch is set.
     *
     * @return true if all parameters are empty.
     */
    public boolean allParamsEmpty()
    {
        // CHECKSTYLE_OFF: LineLength
        return ( StringUtils.isEmpty( family ) && StringUtils.isEmpty( arch ) && StringUtils.isEmpty( name ) && StringUtils.isEmpty( version ) );
        // CHECKSTYLE_ON: LineLength
    }

    /**
     * Creates a Profile object that contains the activation information.
     *
     * @return a properly populated profile to be used for OS validation.
     */
    private Profile createProfile()
    {
        Profile profile = new Profile();
        profile.setActivation( createActivation() );
        return profile;
    }

    /**
     * Creates an Activation object that contains the ActivationOS information.
     *
     * @return a properly populated Activation object.
     */
    private Activation createActivation()
    {
        Activation activation = new Activation();
        activation.setActiveByDefault( false );
        activation.setOs( createOsBean() );
        return activation;
    }

    /**
     * Creates an ActivationOS object containing family, name, version and arch.
     *
     * @return a properly populated ActivationOS object.
     */
    private ActivationOS createOsBean()
    {
        ActivationOS os = new ActivationOS();

        os.setArch( arch );
        os.setFamily( family );
        os.setName( name );
        os.setVersion( version );

        return os;
    }

    /**
     * Helper method to check if the given family is in the following list:
     * <ul>
     * <li>dos</li>
     * <li>mac</li>
     * <li>netware</li>
     * <li>os/2</li>
     * <li>tandem</li>
     * <li>unix</li>
     * <li>windows</li>
     * <li>win9x</li>
     * <li>z/os</li>
     * <li>os/400</li>
     * </ul>
     * Note: '!' is allowed at the beginning of the string and still considered valid.
     *
     * @param theFamily the family to check.
     * @return true if one of the valid families.
     */
    public boolean isValidFamily( String theFamily )
    {

        // in case they are checking !family
        theFamily = StringUtils.stripStart( theFamily, "!" );

        return ( StringUtils.isEmpty( theFamily ) || Os.getValidFamilies().contains( theFamily ) );
    }

    /**
     * Gets the arch.
     *
     * @return the arch
     */
    public String getArch()
    {
        return this.arch;
    }

    /**
     * Sets the arch.
     *
     * @param theArch the arch to set
     */
    public void setArch( String theArch )
    {
        this.arch = theArch;
    }

    /**
     * Gets the family.
     *
     * @return the family
     */
    public String getFamily()
    {
        return this.family;
    }

    /**
     * Sets the family.
     *
     * @param theFamily the family to set
     */
    public void setFamily( String theFamily )
    {
        this.family = theFamily;
    }

    /**
     * Gets the name.
     *
     * @return the name
     */
    public String getName()
    {
        return this.name;
    }

    /**
     * Sets the name.
     *
     * @param theName the name to set
     */
    public void setName( String theName )
    {
        this.name = theName;
    }

    /**
     * Gets the version.
     *
     * @return the version
     */
    public String getVersion()
    {
        return this.version;
    }

    /**
     * Sets the version.
     *
     * @param theVersion the version to set
     */
    public void setVersion( String theVersion )
    {
        this.version = theVersion;
    }

    /**
     * @param display The value for the display.
     */
    public final void setDisplay( boolean display )
    {
        this.display = display;
    }

    public final boolean isDisplay()
    {
        return display;
    }

    @Override
    public String getCacheId()
    {
        // return the hashcodes of all the parameters
        StringBuilder b = new StringBuilder();
        if ( StringUtils.isNotEmpty( version ) )
        {
            b.append( version.hashCode() );
        }
        if ( StringUtils.isNotEmpty( name ) )
        {
            b.append( name.hashCode() );
        }
        if ( StringUtils.isNotEmpty( arch ) )
        {
            b.append( arch.hashCode() );
        }
        if ( StringUtils.isNotEmpty( family ) )
        {
            b.append( family.hashCode() );
        }
        return b.toString();
    }

    @Override
    public boolean isCacheable()
    {
        // the os is not going to change between projects in the same build.
        return true;
    }

    @Override
    public boolean isResultValid( EnforcerRule theCachedRule )
    {
        // i will always return the hash of the parameters as my id. If my parameters are the same, this
        // rule must always have the same result.
        return true;
    }
}
