package org.drools.repository;

import java.io.*;
import java.util.Calendar;
import java.util.Iterator;

import javax.jcr.Binary;
import javax.jcr.Node;
import javax.jcr.PathNotFoundException;
import javax.jcr.Property;
import javax.jcr.RepositoryException;

import org.apache.commons.io.IOUtils;
import org.drools.repository.events.StorageEventManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * The RuleItem class is used to abstract away the details of the underlying JCR repository.
 * It is used to pass information about rules stored in the repository.
 *
 * @author btruitt
 */
public class AssetItem extends CategorisableItem {
    private Logger             log                                  = LoggerFactory.getLogger( AssetItem.class );
    /**
     * The name of the rule node type
     */
    public static final String RULE_NODE_TYPE_NAME                  = "drools:assetNodeType";

    public static final String CONTENT_PROPERTY_NAME                = "drools:content";
    public static final String CONTENT_PROPERTY_BINARY_NAME         = "drools:binaryContent";
    public static final String CONTENT_PROPERTY_ATTACHMENT_FILENAME = "drools:attachmentFileName";

    /**
     * The name of the date effective property on the rule node type
     */
    public static final String DATE_EFFECTIVE_PROPERTY_NAME         = "drools:dateEffective";

    public static final String DISABLED_PROPERTY_NAME               = "drools:disabled";

    /**
     * The name of the date expired property on the rule node type
     */
    public static final String DATE_EXPIRED_PROPERTY_NAME           = "drools:dateExpired";

    public static final String PACKAGE_NAME_PROPERTY                = "drools:packageName";

    /**
     * Constructs a RuleItem object, setting its node attribute to the specified node.
     *
     * @param rulesRepository the rulesRepository that instantiated this object
     * @param node the node in the repository that this RuleItem corresponds to
     * @throws RulesRepositoryException
     */
    public AssetItem(RulesRepository rulesRepository,
                     Node node) throws RulesRepositoryException {
        super( rulesRepository,
               node );
        try {
            //make sure this node is a rule node
            if ( !(this.node.getPrimaryNodeType().getName().equals( RULE_NODE_TYPE_NAME ) || isHistoricalVersion()) ) {
                String message = this.node.getName() + " is not a node of type " + RULE_NODE_TYPE_NAME + " nor nt:version. It is a node of type: " + this.node.getPrimaryNodeType().getName();
                log.error( message );
                throw new RulesRepositoryException( message );
            }
        } catch ( Exception e ) {
            log.error( "Caught exception",
                       e );
            throw new RulesRepositoryException( e );
        }
    }

    public AssetItem() {
        super( null,
               null );
    }




    /**
     * returns the string contents of the rule node.
     * If this is a binary asset, this will return null (use getBinaryContent instead).
     */
    public String getContent() throws RulesRepositoryException {
        try {

            if (StorageEventManager.hasLoadEvent()) {
                return org.apache.commons.io.IOUtils.toString(StorageEventManager.getLoadEvent().loadContent(this));
            }

            if ( isBinary() ) {
                return new String( this.getBinaryContentAsBytes() );
            }
            Node ruleNode = getVersionContentNode();
            if ( ruleNode.hasProperty( CONTENT_PROPERTY_NAME ) ) {
                Property data = ruleNode.getProperty( CONTENT_PROPERTY_NAME );
                return data.getValue().getString();

            } else {
                return "";
            }
        } catch ( Exception e ) {
            log.error( "Caught Exception",
                       e );
            throw new RulesRepositoryException( e );
        }
    }

    /**
     * returns the number of bytes of the content.
     */
    public long getContentLength() {
        try {
            Node ruleNode = getVersionContentNode();
            if ( ruleNode.hasProperty( CONTENT_PROPERTY_BINARY_NAME ) ) {
                Property data = ruleNode.getProperty( CONTENT_PROPERTY_BINARY_NAME );
                return data.getLength();
            } else {
                if ( ruleNode.hasProperty( CONTENT_PROPERTY_NAME ) ) {
                    Property data = ruleNode.getProperty( CONTENT_PROPERTY_NAME );
                    return data.getLength();
                } else {
                    return 0;
                }
            }
        } catch ( RepositoryException e ) {
            log.error( e.getMessage(), e );
            throw new RulesRepositoryException( e );
        }
    }

    /**
     * True if this is a binary asset (or has binary content).
     */
    public boolean isBinary() {
        try {
            Node ruleNode = getVersionContentNode();
            return ruleNode.hasProperty( CONTENT_PROPERTY_BINARY_NAME );
        } catch ( RepositoryException e ) {
            log.error( e.getMessage(), e );
            throw new RulesRepositoryException( e );
        }
    }

    /**
     * If this asset contains binary data, this is how you return it.
     * Otherwise it will return null.
     */
    public InputStream getBinaryContentAttachment() {
        try {
            if (StorageEventManager.hasLoadEvent()) {
                return StorageEventManager.getLoadEvent().loadContent(this);
            }
            Node ruleNode = getVersionContentNode();
            if ( ruleNode.hasProperty( CONTENT_PROPERTY_BINARY_NAME ) ) {
                Property data = ruleNode.getProperty( CONTENT_PROPERTY_BINARY_NAME );
                return data.getBinary().getStream();
            } else {
                if ( ruleNode.hasProperty(CONTENT_PROPERTY_NAME)) {
                    Property data = ruleNode.getProperty( CONTENT_PROPERTY_NAME );
                    return data.getBinary().getStream();
                }
                return null;
            }
        } catch ( Exception e ) {
            log.error( "Caught Exception",
                       e );
            throw new RulesRepositoryException( e );
        }
    }

    /** Get the name of the "file" attachment, if one is set. Null otherwise */
    public String getBinaryContentAttachmentFileName() {
        return getStringProperty( CONTENT_PROPERTY_ATTACHMENT_FILENAME );
    }

    /**
     * This is a convenience method for returning the binary data as a byte array.
     */
    public byte[] getBinaryContentAsBytes() {
        try {
            Node ruleNode = getVersionContentNode();
            if (StorageEventManager.hasLoadEvent()) {
                return IOUtils.toByteArray(StorageEventManager.getLoadEvent().loadContent(this));
            }
            if (isBinary()) {
                    Property data = ruleNode.getProperty( CONTENT_PROPERTY_BINARY_NAME );
                    InputStream in = data.getBinary().getStream();

                    // Create the byte array to hold the data
                    byte[] bytes = new byte[(int) data.getLength()];

                    // Read in the bytes
                    int offset = 0;
                    int numRead = 0;
                    while ( offset < bytes.length && (numRead = in.read( bytes,
                                                                         offset,
                                                                         bytes.length - offset )) >= 0 ) {
                        offset += numRead;
                    }

                    // Ensure all the bytes have been read in
                    if ( offset < bytes.length ) {
                        throw new RulesRepositoryException( "Could not completely read asset " + getName() );
                    }

                    // Close the input stream and return bytes
                    in.close();
                    return bytes;
            } else {
                return getContent().getBytes();
            }
        } catch ( Exception e ) {
            log.error( e.getMessage(), e );
            if ( e instanceof RuntimeException ) throw (RuntimeException) e;
            throw new RulesRepositoryException( e );
        }
    }

    /**
     * @return the date the rule becomes effective
     * @throws RulesRepositoryException
     */
    public Calendar getDateEffective() throws RulesRepositoryException {
        try {
            Node ruleNode = getVersionContentNode();

            Property dateEffectiveProperty = ruleNode.getProperty( DATE_EFFECTIVE_PROPERTY_NAME );
            return dateEffectiveProperty.getDate();
        } catch ( PathNotFoundException e ) {
            // doesn't have this property
            return null;
        } catch ( Exception e ) {
            log.error( "Caught Exception",
                       e );
            throw new RulesRepositoryException( e );
        }
    }

    /**
     * @return if this rule is disabled
     * @throws RulesRepositoryException
     */
    public boolean getDisabled() throws RulesRepositoryException {
        try {
            Node ruleNode = getVersionContentNode();

            Property disabled = ruleNode.getProperty( DISABLED_PROPERTY_NAME );
            return disabled.getBoolean();
        } catch ( PathNotFoundException e ) {
            // doesn't have this property
            return false;
        } catch ( Exception e ) {
            log.error( "Caught Exception",
                       e );
            throw new RulesRepositoryException( e );
        }
    }

    /**
     * Creates a new version of this object's rule node, updating the effective date for the
     * rule node.
     *
     * @param newDateEffective the new effective date for the rule
     * @throws RulesRepositoryException
     */
    public void updateDateEffective(Calendar newDateEffective) throws RulesRepositoryException {
        checkIsUpdateable();
        checkout();
        try {
            this.node.setProperty( DATE_EFFECTIVE_PROPERTY_NAME,
                                   newDateEffective );
        } catch ( RepositoryException e ) {
            log.error( "Caught Exception",
                       e );
            throw new RulesRepositoryException( e );
        }
    }

    /**
     * Creates a new version of this object's rule node, updating the disable value for the
     * rule node.
     *
     * @param disabled is this rule disabled
     * @throws RulesRepositoryException
     */
    public void updateDisabled(boolean disabled) throws RulesRepositoryException {
        checkIsUpdateable();
        checkout();
        try {
            this.node.setProperty( DISABLED_PROPERTY_NAME,
                                   disabled );
        } catch ( RepositoryException e ) {
            log.error( "Caught Exception",
                       e );
            throw new RulesRepositoryException( e );
        }
    }

    /**
     * @return the date the rule becomes expired
     * @throws RulesRepositoryException
     */
    public Calendar getDateExpired() throws RulesRepositoryException {
        try {
            Node ruleNode = getVersionContentNode();

            Property dateExpiredProperty = ruleNode.getProperty( DATE_EXPIRED_PROPERTY_NAME );
            return dateExpiredProperty.getDate();
        } catch ( PathNotFoundException e ) {
            // doesn't have this property
            return null;
        } catch ( Exception e ) {
            log.error( "Caught Exception",
                       e );
            throw new RulesRepositoryException( e );
        }
    }

    /**
     * Creates a new version of this object's rule node, updating the expired date for the
     * rule node.
     *
     * @param newDateExpired the new expired date for the rule
     * @throws RulesRepositoryException
     */
    public void updateDateExpired(Calendar newDateExpired) throws RulesRepositoryException {
        checkout();

        try {
            this.node.setProperty( DATE_EXPIRED_PROPERTY_NAME,
                                   newDateExpired );
        } catch ( Exception e ) {
            log.error( "Caught Exception",
                       e );
            throw new RulesRepositoryException( e );
        }
    }

    /**
     * This will update the asset's content (checking it out if it is not already).
     * This will not save the session or create a new version of the node
     * (this has to be done seperately, as several properties may change as part of one edit).
     * This is only used if the asset is a textual asset. For binary, use the updateBinaryContent method
     * instead.
     */
    public AssetItem updateContent(String newRuleContent) throws RulesRepositoryException {
        checkout();
        try {
            if ( this.isBinary() ) {
                this.updateBinaryContentAttachment( new ByteArrayInputStream( newRuleContent.getBytes() ) );
            }
            this.node.setProperty( CONTENT_PROPERTY_NAME,
                                   newRuleContent );
            return this;
        } catch ( RepositoryException e ) {
            log.error( "Unable to update the asset content",
                       e );
            throw new RulesRepositoryException( e );
        }
    }

    /**
     * If the asset is a binary asset, then use this to update the content
     * (do NOT use text).
     */
    public AssetItem updateBinaryContentAttachment(InputStream data) {
        checkout();
        try {
            Binary binary = this.node.getSession().getValueFactory().createBinary(data);
            this.node.setProperty( CONTENT_PROPERTY_BINARY_NAME,
                binary );
            return this;
        } catch ( RepositoryException e ) {
            log.error( "Unable to update the assets binary content",
                       e );
            throw new RulesRepositoryException( e );
        }
    }

    /**
     * Optionally set the filename to be associated with the binary content.
     */
    public void updateBinaryContentAttachmentFileName(String name) {
        updateStringProperty( name,
                              CONTENT_PROPERTY_ATTACHMENT_FILENAME );
    }

    /**
     * This updates a user defined property (not one of the intrinsic ones).
     */
    public void updateUserProperty(String propertyName,
                                   String value) {
        if ( propertyName.startsWith( "drools:" ) ) {
            throw new IllegalArgumentException( "Can only set the pre defined fields using the appropriate methods." );
        }
        updateStringProperty( value,
                              propertyName );

    }

    /**
     * Nicely formats the information contained by the node that this object encapsulates
     */
    public String toString() {
        try {
            StringBuffer returnString = new StringBuffer();
            returnString.append( "Content of rule item named '" + this.getName() + "':\n" );
            returnString.append( "Content: " + this.getContent() + "\n" );
            returnString.append( "------\n" );

            returnString.append( "Archived: " + this.isArchived() + "\n" );
            returnString.append( "------\n" );

            returnString.append( "Date Effective: " + this.getDateEffective() + "\n" );
            returnString.append( "Date Expired: " + this.getDateExpired() + "\n" );
            returnString.append( "------\n" );

            returnString.append( "Rule state: " );
            StateItem stateItem = this.getState();
            if ( stateItem != null ) {
                returnString.append( this.getState().getName() + "\n" );
            } else {
                returnString.append( "NO STATE SET FOR THIS NODE\n" );
            }
            returnString.append( "------\n" );

            returnString.append( "Rule tags:\n" );
            for ( Iterator<CategoryItem> it = this.getCategories().iterator(); it.hasNext(); ) {
                CategoryItem currentTag = it.next();
                returnString.append( currentTag.getName() + "\n" );
            }
            returnString.append( "--------------\n" );
            return returnString.toString();
        } catch ( Exception e ) {
            throw new RulesRepositoryException( e );
        }
    }

    public VersionableItem getPrecedingVersion() throws RulesRepositoryException {
        try {
            Node precedingVersionNode = this.getPrecedingVersionNode();
            if ( precedingVersionNode != null ) {
                return new AssetItem( this.rulesRepository,
                                      precedingVersionNode );
            } else {
                return null;
            }
        } catch ( Exception e ) {
            log.error( "Caught exception",
                       e );
            throw new RulesRepositoryException( e );
        }
    }

    public VersionableItem getSucceedingVersion() throws RulesRepositoryException {
        try {
            Node succeedingVersionNode = this.getSucceedingVersionNode();
            if ( succeedingVersionNode != null ) {
                return new AssetItem( this.rulesRepository,
                                      succeedingVersionNode );
            } else {
                return null;
            }
        } catch ( Exception e ) {
            log.error( "Caught exception",
                       e );
            throw new RulesRepositoryException( e );
        }
    }

    /**
     * Get the name of the enclosing package.
     * As assets are stored in versionable subfolders, this means walking up 2 levels in the
     * hierarchy to get to the enclosing "package" node.
     */
    public String getPackageName() {
        return super.getStringProperty( PACKAGE_NAME_PROPERTY );
    }

    /**
     * @return A property value (for a user defined property).
     */
    public String getUserProperty(String property) {
        return getStringProperty( property );
    }

    /**
     * This will remove the item.
     * The repository will need to be saved for this to take effect.
     * Typically the package that contains this should be versioned before removing this,
     * to make it easy to roll back.
     */
    public void remove() {

        if (StorageEventManager.hasSaveEvent()) {
            StorageEventManager.getSaveEvent().onAssetDelete(this);
        }

        checkIsUpdateable();
        if ( this.getDateExpired() != null ) {
            if ( Calendar.getInstance().before( this.getDateExpired() ) ) {
                throw new RulesRepositoryException( "Can't delete an item before its expiry date." );
            }
        }
        try {
            this.node.remove();
        } catch ( RepositoryException e ) {
            throw new RulesRepositoryException( e );
        }
    }

    /**
     *
     * @return An iterator over the nodes history.
     */
    public AssetHistoryIterator getHistory() {
        return new AssetHistoryIterator( this.rulesRepository,
                                         this.node );
    }

    /**
     * This will get the package an asset item belongs to.
     */
    public PackageItem getPackage() {

        try {
            if ( this.isHistoricalVersion() ) {
            	return this.rulesRepository.loadPackage(this.getPackageName());
            }
            return new PackageItem( this.rulesRepository,
                                    this.node.getParent().getParent() );
        } catch ( RepositoryException e ) {
            throw new RulesRepositoryException( e );
        }
    }

    /**
     * This converts a "filename" to an asset name.
     *
     * File name is foo.drl -> ["foo", "drl"]
     *
     * File name is foo.bar.xls -> ["foo", "bar.xls"]
     * @param fileName
     * @return
     */
    public static String[] getAssetNameFromFileName(String fileName) {

        String[] r = new String[]{"", ""};
        char[] cs = fileName.toCharArray();
        boolean name = true;
        for ( int i = 0; i < cs.length; i++ ) {
            if ( name && cs[i] == '.' ) {
                String rhs = fileName.substring( i );
                if ( rhs.contains( "_" ) || rhs.contains( " " ) ) {
                    r[0] = r[0] + '.'; //its part of the name
                } else {
                    name = false;
                }
            } else if ( name ) {
                r[0] = r[0] + cs[i];
            } else {
                r[1] = r[1] + cs[i];
            }
        }
        return r;

    }

}