/*
Copyright (C) 2000-2010  Ministere de la culture et de la communication (France), AJLSM
See LICENCE file
*/
package fr.gouv.culture.sdx.search.lucene;


import java.io.File;
import java.text.Collator;
import java.util.Date;
import java.util.Locale;

import org.apache.avalon.framework.configuration.Configurable;
import org.apache.avalon.framework.configuration.Configuration;
import org.apache.avalon.framework.configuration.ConfigurationException;
import org.apache.avalon.framework.context.ContextException;
import org.apache.avalon.framework.context.Contextualizable;
import org.apache.avalon.framework.context.DefaultContext;
import org.apache.avalon.framework.logger.LogEnabled;

import fr.gouv.culture.sdx.exception.SDXException;
import fr.gouv.culture.sdx.exception.SDXExceptionCode;
import fr.gouv.culture.sdx.search.lucene.analysis.Analyzer;
import fr.gouv.culture.sdx.search.lucene.analysis.AnalyzerManager;
import fr.gouv.culture.sdx.utils.AbstractSdxObject;
import fr.gouv.culture.sdx.utils.Utilities;
import fr.gouv.culture.sdx.utils.configuration.ConfigurationUtils;
import fr.gouv.culture.sdx.utils.constants.ContextKeys;
import fr.gouv.culture.sdx.utils.constants.Node;
import fr.gouv.culture.sdx.utils.logging.LoggingUtils;


/**
 * A field for indexing.
 */
public class Field extends AbstractSdxObject implements LogEnabled, Contextualizable, Configurable, java.io.Serializable {

    /**String representation of the pipeline class name suffix. */
    public static final String CLASS_NAME_SUFFIX = "Field";

    /** Avalon super.getLog() to write information. */
    private transient org.apache.avalon.framework.logger.Logger logger;

    /**The analyzer for this field. */
    private Analyzer analyzer;

    /**The field name*/
    private String name;

    /** Indique s'il doit etre retourne dans la liste des resultats. */
    private boolean inBrief;
    // MAJ Lucene 2.1.0
    private org.apache.lucene.document.Field.Store store;

    /** Indique si les termVectors doivent etre stockes pour ce champs     */
    private boolean storeTermVector;
    // MAJ Lucene 2.1.0
    private org.apache.lucene.document.Field.TermVector termVector;

    /** Le type de champ dont il s'agit. */
    private int fieldType;

    /** S'il s'agit du champ par defaut. */
    private boolean isDefault;

    /** The field's locale. */
    private Locale locale;

    /**String representation of the 'field' attribute named 'type'. */
    private final String ATTRIBUTE_TYPE = Node.Name.TYPE;

    /**String representation of the 'field' attribute named 'default'. */
    private final String ATTRIBUTE_DEFAULT = "default";

    /**String representation of the 'field' attribute named 'brief'. */
    private final String ATTRIBUTE_BRIEF = "brief";

    /**String representation of the 'field' attribute named 'storeTermVector'. */
    private final String ATTRIBUTE_TERMVECTOR = "storeTermVector";

    /**String representation of the 'field' attribute named 'analyzerConf'. */
    private final String ATTRIBUTE_ANALYZER_CONF = "analyzerConf";

    /**String representation of the 'field' attribute named 'analyzerClass'. */
    private final String ATTRIBUTE_ANALYZER_CLASS = "analyzerClass";

    /**String representation of the field type. */
    private static final String FIELD_TYPE_WORD = "WORD";

    /**String representation of the field type. */
    private static final String FIELD_TYPE_FIELD = "FIELD";

    /**String representation of the field type. */
    private static final String FIELD_TYPE_DATE = "DATE";

    /**String representation of the field type. */
    private static final String FIELD_TYPE_UNINDEXED = "UNINDEXED";

    /**String representation of the field type. */
    private static final String FIELD_TYPE_XML = "XML";

    /**The application's properties.
     * Since _context is currently only used at configuration time it is ok for it to be <code>transient</code>
     * */
    private transient DefaultContext _context;

    private String analyzerClassName = null;

    private String analyzerConfFilePath = null;

    /** Field type "word". */
    public static final int WORD = 0;

    /** Field type "field" */
    public static final int FIELD = 1;

    /** Field type "date". */
    public static final int DATE = 2;

    /** Field type "unindexed".*/
    public static final int UNINDEXED = 3;

    /** Field type "xml".*/
    public static final int XML = 4;

    /**Constructor to satisfy needs of RMI and Serializable implementation*/
    public Field() {
    }


    /**Builds a field object with params
     *
     * @param locale      The locale to use as a default if none specified in the configuration
     * @param analyzer    The default analyzer to use if none specified in the configuration
     * @param analyzerClass The fully qualified class name to use as a default
     * @param analyzerConfFile The anlayzer configuration file path to use as a default
     */
    public void setUp(Locale locale, Analyzer analyzer, String analyzerClass, String analyzerConfFile) {
        //setting the  defaults
        this.locale = locale;
        this.analyzer = analyzer;
        this.analyzerClassName = analyzerClass;
        this.analyzerConfFilePath = analyzerConfFile;
    }

    protected String getClassNameSuffix(){
    	return Field.CLASS_NAME_SUFFIX;
    }



    /** Set's the super.getLog() for the field.
     *
     * @param logger    The super.getLog() to use
     */
    public void enableLogging(org.apache.avalon.framework.logger.Logger logger) {
        this.logger = logger;
    }


    public void contextualize(org.apache.avalon.framework.context.Context context) throws ContextException {
        _context = new DefaultContext(context);
    }

    protected DefaultContext getContext() {
        return _context;
    }

    /**Configures the field
     *
     * @param configuration
     * @throws ConfigurationException
     */
    public void configure(Configuration configuration) throws ConfigurationException {
        ConfigurationUtils.checkConfiguration(configuration);

        //getting the field name
        this.name = configuration.getAttribute(Node.Name.NAME, null);
        //backward compatibility : try to use old attribute name
        if (this.name == null) this.name = configuration.getAttribute(Node.Name.CODE);
        //verifying we have a value
        ConfigurationUtils.checkConfAttributeValue(Node.Name.NAME, this.name, configuration.getLocation());
        //getting and setting the field type
        //TODO?:what should the default field type be if not defined, for now using WORD?-rbp
        setFieldType(configuration.getAttribute(ATTRIBUTE_TYPE, FIELD_TYPE_WORD));
        //getting the 'default' attr
        isDefault = configuration.getAttributeAsBoolean(ATTRIBUTE_DEFAULT, false);
        //getting 'inBrief' attr
        inBrief = configuration.getAttributeAsBoolean(ATTRIBUTE_BRIEF, false);
        // MAJ Lucene 2.1.0
        	 if ( this.fieldType == XML )	store = org.apache.lucene.document.Field.Store.YES; // force store for XML field type because Lucene 2.1.0 start an exception if such a field is not
        else if ( inBrief )					store = org.apache.lucene.document.Field.Store.YES;
        else								store = org.apache.lucene.document.Field.Store.NO;
        // TODO: store can accept an other value : COMPRESS [MP]
        //getting 'storeTermVector' attr
        storeTermVector = configuration.getAttributeAsBoolean(ATTRIBUTE_TERMVECTOR, false);
        if ( storeTermVector )	termVector = org.apache.lucene.document.Field.TermVector.YES;
        else					termVector = org.apache.lucene.document.Field.TermVector.NO;
        // FIXME: termVector can accept more parameters : WITH_OFFSETS, WITH_POSITIONS, WITH_POSITIONS_OFFSETS  
        //getting 'analyzerClass' attr, default is null if it doesn't exist
        String localClassName = configuration.getAttribute(ATTRIBUTE_ANALYZER_CLASS, this.analyzerClassName);
        //getting 'analyzerConf' attr, default is null if it doesn't exist
        String confFileRelPath = configuration.getAttribute(ATTRIBUTE_ANALYZER_CONF, this.analyzerConfFilePath);


        Locale fieldLocale = Utilities.buildLocale(configuration, this.locale);
        //we set our local field
        if (fieldLocale != null) this.locale = fieldLocale;

        File localAnalyzerConfFile = null;
        try {
            if (confFileRelPath != null) {
                //verifying the attribute, if it was specified
                ConfigurationUtils.checkConfAttributeValue(ATTRIBUTE_ANALYZER_CONF, confFileRelPath, configuration.getLocation());
                //getting the file
                localAnalyzerConfFile = Utilities.resolveFile(null, configuration.getLocation(), getContext(), confFileRelPath, false);
            }
        } catch (ConfigurationException e) {
            //we want to continue
            LoggingUtils.logWarn(logger, null, e);
        } catch (SDXException e) {
            //we want to continue
            LoggingUtils.logWarn(logger, null, e);
        }

        try {
            AnalyzerManager analyzeMgr = (AnalyzerManager) getContext().get(ContextKeys.SDX.Framework.ANALYZER_MGR);
            if (Utilities.checkString(localClassName))
                this.analyzer = analyzeMgr.getAnalyzer(localClassName, localAnalyzerConfFile);
//            else if (localAnalyzerConfFile != null) // if we have a configuration file we build from the locale and the confile
            else if (localAnalyzerConfFile != null)// if we have a configuration file we build from the locale and the confile
                this.analyzer = analyzeMgr.getAnalyzer(locale, localAnalyzerConfFile);
            //otherwise we use the default analyzer provided by the parent fieldList object to our constructor
        } catch (SDXException sdxE) {
            throw new ConfigurationException(sdxE.getMessage(), sdxE.fillInStackTrace());
        } catch (ContextException e) {
            throw new ConfigurationException(e.getMessage(), e.fillInStackTrace());
        }

        //giving the analyzer a super.getLog()
        this.analyzer.enableLogging(logger);

    }

    /**Provides access to the analyzer for this field.
     *
     * @return The analyzer for this field.
     */
    public Analyzer getAnalyzer() {
        /*TODO should we do the below in a switch?
        if (this.fieldType == FIELD|| this.fieldType == DATE || this.fieldType == UNINDEXED)
            return null;//the above field types aren't analyzed in lucene
        */
        return analyzer;

    }

    public void setAnalyzer(Analyzer analyzer) {
        this.analyzer = analyzer; //TODO : consider a reconfiguration ? -pb
    }


    /**
     * Returns the field's code (name)
     */
    public String getCode() {
        return name;
    }

    /*
    *	Dans le cas d'un champ de type date, si le contenu peut etre interprete
     *	comme une date on l'insere de cette facon, sinon on va l'inserer comme
     *	un champ keyword.
    */

    /**
     *	Returns the Lucene field for the search field.
     *
     * In the case of a field of type "date", if the contents can be interpreted
     * as a date it will be  inserted this way, if not it will be inserted like
     * a field of type "keyword".
     *
     *@param content The field's content.
     * @return org.apache.lucene.document.Field
     * @throws SDXException 
     */
    public org.apache.lucene.document.Field getLuceneField(String content) throws SDXException {
        if (!Utilities.checkString(content)) return null;

        switch (fieldType) {
            case DATE:
                // We will try to get a date
                Date date = fr.gouv.culture.sdx.utils.Date.parseDate(content);
                if (date != null) {
                    String dateString = null;
                    try {
                        dateString = DateField.dateToString(date);
                    } catch (RuntimeException e) {
                        String[] args = {"There was a problem with the the date provided, " + content + " : " + e.getMessage()};
                        throw new SDXException(this.logger, SDXExceptionCode.ERROR_GENERIC, args, e);
                    }
                    // MAJ Lucene 2.1.0
                    //return new org.apache.lucene.document.Field(name, dateString, inBrief, true, false, false);
                    return new org.apache.lucene.document.Field(name, dateString, this.store, org.apache.lucene.document.Field.Index.UN_TOKENIZED, org.apache.lucene.document.Field.TermVector.NO);
                } else
                	// MAJ Lucene 2.1.0
                    //return new org.apache.lucene.document.Field(name, content, inBrief, true, false, false);
                	return new org.apache.lucene.document.Field(name, content, this.store, org.apache.lucene.document.Field.Index.UN_TOKENIZED, org.apache.lucene.document.Field.TermVector.NO);

            case FIELD:
            	// MAJ Lucene 2.1.0
            	//return new org.apache.lucene.document.Field(name, content, inBrief, true, false, false);
            	return new org.apache.lucene.document.Field(name, content, this.store, org.apache.lucene.document.Field.Index.UN_TOKENIZED, org.apache.lucene.document.Field.TermVector.NO);
            case WORD:
            	// MAJ Lucene 2.1.0
                //return new org.apache.lucene.document.Field(name, content, inBrief, true, true, this.storeTermVector);
            	return new org.apache.lucene.document.Field(name, content, this.store, org.apache.lucene.document.Field.Index.TOKENIZED, this.termVector);
            case UNINDEXED:
            case XML:
            	// MAJ Lucene 2.1.0
                //return new org.apache.lucene.document.Field(name, content, inBrief, false, false, false);
            	return new org.apache.lucene.document.Field(name, content, org.apache.lucene.document.Field.Store.YES, org.apache.lucene.document.Field.Index.NO, this.termVector);	// INFO: force store for XML field to avoid Lucene exception
            default:		// Shoud never happen, but still we return a tokenized field but unstore
            	// MAJ Lucene 2.1.0
                //return new org.apache.lucene.document.Field(name, content, false, true, true, false);
            	return new org.apache.lucene.document.Field(name, content, org.apache.lucene.document.Field.Store.NO, org.apache.lucene.document.Field.Index.TOKENIZED, org.apache.lucene.document.Field.TermVector.NO);
        }

    }

    /**
     *	Indicates if this field is defined as a default field.
     */
    public boolean isDefault() {
        return isDefault;
    }

    /**
     *	Returns the type of the field.
     */
    public int getFieldType() {
        return fieldType;
    }

    /**
     *	Returns the type Name of the field.
     */
    public String getTypeName() {
        if (this.fieldType == WORD) return FIELD_TYPE_WORD.toLowerCase(this.locale);
        if (this.fieldType == FIELD) return FIELD_TYPE_FIELD.toLowerCase(this.locale);
        if (this.fieldType == DATE) return FIELD_TYPE_DATE.toLowerCase(this.locale);
        if (this.fieldType == UNINDEXED) return FIELD_TYPE_UNINDEXED.toLowerCase(this.locale);
        if (this.fieldType == XML) return FIELD_TYPE_XML.toLowerCase(this.locale);
        return null;
    }

    /** Provides access to the locale for this field.
     *
     * @return
     */
    public Locale getLocale() {
        return locale;
    }

    /**
     * Returns a collator for sorting this field.
     */
    public Collator getCollator() {
        return Collator.getInstance(locale);
    }

    /**Sets the type for this field
     *
     * @param type  The field type as a string from our configuration object (element in app.xconf)
     */
    public void setFieldType(String type) {
        if (type.equalsIgnoreCase(FIELD_TYPE_WORD)) {
            fieldType = WORD;
        } else if (type.equalsIgnoreCase(FIELD_TYPE_FIELD)) {
            fieldType = FIELD;
        } else if (type.equalsIgnoreCase(FIELD_TYPE_DATE)) {
            fieldType = DATE;
        } else if (type.equalsIgnoreCase(FIELD_TYPE_UNINDEXED)) {
            fieldType = UNINDEXED;
        }else if (type.equalsIgnoreCase(FIELD_TYPE_XML)) {
            fieldType = XML;
        } else
            fieldType = WORD;
    }

    public boolean isInBrief() {
        return inBrief;
    }

    public boolean getStoreTermVector() {
        return storeTermVector;
    }

	/* (non-Javadoc)
	 * @see fr.gouv.culture.sdx.utils.AbstractSdxObject#initToSax()
	 */
	protected boolean initToSax() {
		//this._xmlizable_objects.put("analyser_class_name", this.analyzerClassName);
		//this._xmlizable_objects.put("analyser_conf_file_path", this.analyzerConfFilePath);
		this._xmlizable_objects.put("name", this.name);
		this._xmlizable_objects.put("type_name", this.getTypeName());
		if (this.getTypeName().equalsIgnoreCase(FIELD_TYPE_WORD))
			this._xmlizable_objects.put("analyser", this.analyzer);
		this._xmlizable_objects.put("storeTermVector", String.valueOf(this.getStoreTermVector()));
		this._xmlizable_objects.put("in_brief", String.valueOf(this.isInBrief()));
		this._xmlizable_objects.put("is_default", String.valueOf(this.isDefault()));
		this._xmlizable_objects.put("locale", this.locale.toString());
		return true;
	}


	/**Init the LinkedHashMap _xmlizable_volatile_objects with the objects in order to describ them in XML
	 * Some objects need to be refresh each time a toSAX is called*/
	protected void initVolatileObjectsToSax() {
		this._xmlizable_objects.put("storeTermVector", String.valueOf(this.getStoreTermVector()));
		this._xmlizable_objects.put("in_brief", String.valueOf(this.isInBrief()));
		this._xmlizable_objects.put("is_default", String.valueOf(this.isDefault()));
		this._xmlizable_objects.put("locale", this.locale.toString());
	}

}
