/* -*- tab-width: 4 -*-
 *
 * Electric(tm) VLSI Design System
 *
 * File: PrefPackage.java
 * Written by: Dmitry Nadezhin.
 *
 * Copyright (c) 2009, Static Free Software. All rights reserved.
 *
 * Electric(tm) is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * Electric(tm) is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.sun.electric.database.text;

import com.sun.electric.database.id.ArcProtoId;
import com.sun.electric.database.id.LayerId;
import com.sun.electric.database.id.PrimitiveNodeId;
import com.sun.electric.database.id.TechId;
import com.sun.electric.technology.Technology;

import java.io.Serializable;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Field;
import java.util.prefs.Preferences;

/**
 * Abstract class to define a package of appearence options.
 * Subclass can define Java annotations on its fields, describing
 * their persistence representation in Java Preferences.
 */
public abstract class PrefPackage implements Serializable, Cloneable {

    protected static final String TECH_NODE = Technology.TECH_NODE;
    protected static final String USER_NODE = "tool/user";
    protected static final String IO_NODE = "tool/io";

    /**
     * Protected constuctor fills annotated option fields of the subclass from Preferences subtree.
     * Now possible root can be obtained by {@link com.sun.electric.database.text.Pref#getPrefRoot()}
     * and {@link com.sun.electric.database.text.Pref#getFactoryPrefRoot()} methods.
     * @param factory use the Factory Pref root
     */
    protected PrefPackage(boolean factory) {
        this(factory ? getFactoryPrefRoot() : getPrefRoot());
    }

    protected PrefPackage withField(String fieldName, Object value) {
        try {
            PrefPackage that = (PrefPackage) clone();
            Field field = findField(fieldName);
            field.setAccessible(true);
            field.set(that, value);
            return that;
        } catch (CloneNotSupportedException e) {
            throw new AssertionError();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
            throw new AssertionError();
        }
    }

    private Field findField(String fieldName) {
        Class cls = getClass();
//        Field fld = null;
        while (cls != PrefPackage.class) {
            try {
                return cls.getDeclaredField(fieldName);
            } catch (NoSuchFieldException e) {
                cls = cls.getSuperclass();
            }
        }
        return null;
    }

    /**
     * Protected constuctor fills annotated option fields of the subclass from Preferences subtree.
     * Now possible root can be obtained by {@link com.sun.electric.database.text.Pref#getPrefRoot()}
     * and {@link com.sun.electric.database.text.Pref#getFactoryPrefRoot()} methods.
     * @param prefRoot the root of the Preferences subtree.
     */
    protected PrefPackage(Preferences prefRoot) {
        Class cls = getClass();
        for (Field field : cls.getDeclaredFields()) {
            try {
                BooleanPref ba = field.getAnnotation(BooleanPref.class);
                if (ba != null) {
                    assert field.getType() == Boolean.TYPE;
                    field.setAccessible(true);
                    field.setBoolean(this, prefRoot.node(ba.node()).getBoolean(ba.key(), ba.factory()));
                }
                IntegerPref ia = field.getAnnotation(IntegerPref.class);
                if (ia != null) {
                    assert field.getType() == Integer.TYPE;
                    field.setAccessible(true);
                    field.setInt(this, prefRoot.node(ia.node()).getInt(ia.key(), ia.factory()));
                }
                LongPref la = field.getAnnotation(LongPref.class);
                if (la != null) {
                    assert field.getType() == Long.TYPE;
                    field.setAccessible(true);
                    field.setLong(this, prefRoot.node(la.node()).getLong(la.key(), la.factory()));
                }
                DoublePref da = field.getAnnotation(DoublePref.class);
                if (da != null) {
                    assert field.getType() == Double.TYPE;
                    field.setAccessible(true);
                    field.setDouble(this, prefRoot.node(da.node()).getDouble(da.key(), da.factory()));
                }
                StringPref sa = field.getAnnotation(StringPref.class);
                if (sa != null) {
                    assert field.getType() == String.class;
                    field.setAccessible(true);
                    field.set(this, prefRoot.node(sa.node()).get(sa.key(), sa.factory()));
                }
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * Store annotated option fields of the subclass into the speciefied Preferences subtree.
     * @param prefRoot the root of the Preferences subtree.
     * @param removeDefaults remove from the Preferences subtree options which have factory default value.
     */
    protected void putPrefs(Preferences prefRoot, boolean removeDefaults) {
        Class cls = getClass();
        for (Field field : cls.getDeclaredFields()) {
            try {
                BooleanPref ba = field.getAnnotation(BooleanPref.class);
                if (ba != null) {
                    assert field.getType() == Boolean.TYPE;
                    field.setAccessible(true);
                    boolean v = field.getBoolean(this);
                    Preferences prefs = prefRoot.node(ba.node());
                    if (removeDefaults && v == ba.factory()) {
                        prefs.remove(ba.key());
                    } else {
                        prefs.putBoolean(ba.key(), v);
                    }
                }
                IntegerPref ia = field.getAnnotation(IntegerPref.class);
                if (ia != null) {
                    assert field.getType() == Integer.TYPE;
                    field.setAccessible(true);
                    int v = field.getInt(this);
                    Preferences prefs = prefRoot.node(ia.node());
                    if (removeDefaults && v == ia.factory()) {
                        prefs.remove(ia.key());
                    } else {
                        prefs.putInt(ia.key(), v);
                    }
                }
                LongPref la = field.getAnnotation(LongPref.class);
                if (la != null) {
                    assert field.getType() == Long.TYPE;
                    field.setAccessible(true);
                    long v = field.getLong(this);
                    Preferences prefs = prefRoot.node(la.node());
                    if (removeDefaults && v == la.factory()) {
                        prefs.remove(la.key());
                    } else {
                        prefs.putLong(la.key(), v);
                    }
                }
                DoublePref da = field.getAnnotation(DoublePref.class);
                if (da != null) {
                    assert field.getType() == Double.TYPE;
                    field.setAccessible(true);
                    double v = field.getDouble(this);
                    Preferences prefs = prefRoot.node(da.node());
                    if (removeDefaults && v == da.factory()) {
                        prefs.remove(da.key());
                    } else {
                        prefs.putDouble(da.key(), v);
                    }
                }
                StringPref sa = field.getAnnotation(StringPref.class);
                if (sa != null) {
                    assert field.getType() == String.class;
                    field.setAccessible(true);
                    String v = (String) field.get(this);
                    Preferences prefs = prefRoot.node(sa.node());
                    if (removeDefaults && v.equals(sa.factory())) {
                        prefs.remove(sa.key());
                    } else {
                        prefs.put(sa.key(), v);
                    }
                }
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * Store annotated option fields of the subclass into the speciefied Preferences subtree.
     * @param prefRoot the root of the Preferences subtree.
     * @param removeDefaults remove from the Preferences subtree options which have factory default value.
     */
    public static void lowLevelPutPrefs(PrefPackage prefPackage, Preferences prefRoot, boolean removeDefaults) {
        prefPackage.putPrefs(prefRoot, removeDefaults);
    }

    /**
     * Returns the root of Preferences subtree with Electric options.
     * Currently this is "/com/sun/electric/" subtree.
     * @return the root of Preferences subtree with Electric options.
     */
    public static Preferences getPrefRoot() {
        return Pref.getPrefRoot();
    }

    /**
     * Returns the root of dummy Preferences subtree with factory default Electric options.
     * @return the root of Preferences subtree with factory default Electric options.
     */
    public static Preferences getFactoryPrefRoot() {
        return Pref.getFactoryPrefRoot();
    }

    protected String getKey(String what, TechId techId) {
        int len = what.length() + techId.techName.length();
        StringBuilder sb = new StringBuilder(len);
        sb.append(what);
        sb.append(techId.techName);
        assert sb.length() == len;
        return sb.toString();
    }

    protected String getKey(String what, LayerId layerId) {
        int len = what.length() + layerId.fullName.length() + 1;
        StringBuilder sb = new StringBuilder(len);
        sb.append(what);
        sb.append(layerId.name);
        sb.append("IN");
        sb.append(layerId.techId.techName);
        assert sb.length() == len;
        return sb.toString();
    }

    protected String getKey(String what, PrimitiveNodeId pnId) {
        int len = what.length() + pnId.fullName.length() + 4;
        StringBuilder sb = new StringBuilder(len);
        sb.append(what);
        sb.append("For");
        sb.append(pnId.name);
        sb.append("IN");
        sb.append(pnId.techId.techName);
        assert sb.length() == len;
        return sb.toString();
    }

    protected String getKey(String what, ArcProtoId apId) {
        int len = what.length() + apId.fullName.length() + 4;
        StringBuilder sb = new StringBuilder(len);
        sb.append(what);
        sb.append("For");
        sb.append(apId.name);
        sb.append("IN");
        sb.append(apId.techId.techName);
        assert sb.length() == len;
        return sb.toString();
    }

    /**
     * Indicates that a field declaration is intended to keep value of a boolean option.
     */
    @Target(ElementType.FIELD)
    @Retention(RetentionPolicy.RUNTIME)
    protected @interface BooleanPref {

        /**
         * Preferences node path where the annotated option is stored.
         * The path is relative.
         */
        public String node();

        /**
         * Preferences key with which the annotated option is associated.
         */
        public String key();

        /**
         * Factory default value of the annotated option.
         */
        public boolean factory();
    }

    /**
     * Indicates that a field declaration is intended to keep value of an integer option.
     */
    @Target(ElementType.FIELD)
    @Retention(RetentionPolicy.RUNTIME)
    protected @interface IntegerPref {

        /**
         * Preferences node path where the annotated option is stored.
         * The path is relative.
         */
        public String node();

        /**
         * Preferences key with which the annotated option is associated.
         */
        public String key();

        /**
         * Factory default value of the annotated option.
         */
        public int factory();
    }

    /**
     * Indicates that a field declaration is intended to keep value of a long option.
     */
    @Target(ElementType.FIELD)
    @Retention(RetentionPolicy.RUNTIME)
    protected @interface LongPref {

        /**
         * Preferences node path where the annotated option is stored.
         * The path is relative.
         */
        public String node();

        /**
         * Preferences key with which the annotated option is associated.
         */
        public String key();

        /**
         * Factory default value of the annotated option.
         */
        public long factory();
    }

    /**
     * Indicates that a field declaration is intended to keep value of a double option.
     */
    @Target(ElementType.FIELD)
    @Retention(RetentionPolicy.RUNTIME)
    protected @interface DoublePref {

        /**
         * Preferences node path where the annotated option is stored.
         * The path is relative.
         */
        public String node();

        /**
         * Preferences key with which the annotated option is associated.
         */
        public String key();

        /**
         * Factory default value of the annotated option.
         */
        public double factory();
    }

    /**
     * Indicates that a field declaration is intended to keep value of a String option.
     */
    @Target(ElementType.FIELD)
    @Retention(RetentionPolicy.RUNTIME)
    protected @interface StringPref {

        /**
         * Preferences node path where the annotated option is stored.
         * The path is relative.
         */
        public String node();

        /**
         * Preferences key with which the annotated option is associated.
         */
        public String key();

        /**
         * Factory default value of the annotated option.
         */
        public String factory();
    }
}
