/*
 * DefaultResource class.
 *
 * Copyright (C) 2007 SATOH Takayuki All Rights Reserved.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 */
package ts.util.resource;

import java.util.List;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.Map;
import ts.util.text.StringOperation;

/**
 * 階層構造をもった要素である{@link ts.util.resource.Element Element}
 * オブジェクトを使ってリソースデータを管理するリソースクラス。
 * <br>
 * 本来階層構造を持たないデータをリソースオブジェクトで扱うことができるように
 * するために用意された抽象クラスである。
 *
 * @author  佐藤隆之. 
 * @version $Revision: 1.3 $, $Date: 2011-08-01 15:29:31 $
 */
public abstract class DefaultResource extends AbstractResource
{
  /** ルート要素。 */
  private Element rootElement_ ;

  /** 基底要素。 */
  private Element baseElement_ ;

  /**
   * デフォルトコンストラクタ。
   */
  public DefaultResource()
  {
    rootElement_ = Element.createRootElement();
    baseElement_ = rootElement_ ;
  }

  /**
   * コピーコンストラクタ。
   *
   * @param  res コピー元のリソースオブジェクト。
   * @throws AssertionError 引数がヌルの場合（デバッグモードのみ）。
   */
  public DefaultResource(DefaultResource res)
  {
    super(res);

    rootElement_ = res.getRootElement();
    baseElement_ = res.getBaseElement();
  }

  /**
   * このオブジェクトを再初期化する。
   */
  protected void renew()
  {
    rootElement_ = Element.createRootElement();
    baseElement_ = rootElement_ ;
  }

  /**
   * ルート要素を取得する。
   *
   * @return ルート要素オブジェクト。
   */
  protected Element getRootElement()
  {
    return rootElement_ ;
  }

  /**
   * 基底要素を取得する。
   *
   * @return 基底要素オブジェクト。
   */
  protected Element getBaseElement()
  {
    return baseElement_ ;
  }

  /**
   * 基底要素の名前を取得する。
   *
   * @return 基底要素の名前。
   */
  public String getBaseElementName()
  {
    return getBaseElement().getName();
  }

  /**
   * このオブジェクトと同じクラスのリソースオブジェクトを作成する。
   *
   * @return このオブジェクトと同じクラスのリソースオブジェクト。
   */
  protected abstract DefaultResource createResource();

  /**
   * 指定されたキーに最初に該当する要素を取得する。
   * <br>
   * 該当する要素が筆頭も存在しない場合はヌルを返す。
   * 引数のキーが不正な場合は例外をスローする。
   *
   * @param  key キー。
   * @return 引数のキーに最初に対応する要素。
   * @throws IllegalKeyException キーが不正な場合。
   * @throws AssertionError 引数がヌルの場合（デバッグモードのみ）。
   */
  protected Element findFirstElement(String key) throws IllegalKeyException
  {
    assert (key != null) : "@param:key is null.";

    if (StringOperation.isEmpty(key)) {
      return getBaseElement();
    }

    return findFirstElement(getBaseElement(), splitKey(key), 0);
  }

  /**
   * キー要素リストに最初に該当する下位要素を再帰的に探索する。
   *
   * @param  elem 要素。
   * @param  keyElemLst キー要素リスト。
   * @param  index 現在探索中のキー要素のインデックス。
   * @return キー要素リストに該当する要素。
   * @throws NullPointerException 引数がヌルの場合。
   * @throws IndexOutOfBoundsException キー要素のインデックスが負の値の場合。
   */
  private Element findFirstElement(
    Element elem, List<String> keyElemLst, int index
  ) throws NullPointerException, IndexOutOfBoundsException
  {
    if (index >= keyElemLst.size()) {
      return elem;
    }

    String keyElem = keyElemLst.get(index);
    String elemName = getNameOfKeyElement(keyElem);
    List<String[]> attrLst = listAttributesOfKeyElement(keyElem);

    List<Element> eL = elemName.equals("*") ? 
      elem.getAllChildren() : elem.getChildren(elemName);

    NEXT_ELEM:
    for (Element e : eL) {
      AttributeMap attrMap = e.getAttributes();
      for (String[] attr : attrLst) {
        if (! attr[1].equals(attrMap.get(attr[0]))) {
          continue NEXT_ELEM;
        }
      }

      Element child = findFirstElement(e, keyElemLst, index + 1);
      if (child != null) {
        return child;
      }
    }

    return null;
  }

  /**
   * 指定されたキーに該当する全ての要素を取得する。
   * <br>
   * 該当する要素が一つも存在しない場合は空のリストを返す。
   * 引数のキーが不正な場合は例外をスローする。
   *
   * @param  key キー。
   * @return 引数のキーに対応する全ての要素を格納するリスト。
   * @throws IllegalKeyException キーが不正な場合。
   * @throws AssertionError 引数がヌルの場合（デバッグモードのみ）。
   */
  protected List<Element> findAllElements(String key) throws IllegalKeyException
  {
    assert (key != null) : "@param:key is null.";

    List<Element> elemLst = new LinkedList<Element>();

    if (StringOperation.isEmpty(key)) {
      elemLst.add(getBaseElement());
      return elemLst;
    }

    List<String> keyElemLst = splitKey(key);
    findAllElements(elemLst, getBaseElement(), keyElemLst, 0);
    return elemLst;
  }

  /**
   * キー要素リストに該当する全ての下位要素を再帰的に探索して、リストに格納
   * する。
   *
   * @param  elemLst 該当する要素を格納するリスト。
   * @param  elem 要素。
   * @param  keyElemLst キー要素リスト。
   * @param  index 現在探索中のキー要素のインデックス。
   * @throws NullPointerException 引数がヌルの場合。
   * @throws IndexOutOfBoundsException インデックスが負の値の場合。
   */
  private void findAllElements(
    List<Element> elemLst, Element elem, List<String> keyElemLst, int index
  ) throws NullPointerException, IndexOutOfBoundsException
  {
    if (index >= keyElemLst.size()) {
      elemLst.add(elem);
      return;
    }

    String keyElem = keyElemLst.get(index);
    String elemName = getNameOfKeyElement(keyElem);
    List<String[]> attrLst = listAttributesOfKeyElement(keyElem);

    List<Element> eL = elemName.equals("*") ? 
      elem.getAllChildren() : elem.getChildren(elemName);

    NEXT_ELEM:
    for (Element e : eL) {
      if (attrLst.size() > 0) {
        AttributeMap attrMap = e.getAttributes();
        for (String[] attr : attrLst) {
          if (! attr[1].equals(attrMap.get(attr[0]))) {
            continue NEXT_ELEM;
          }
        }
      }

      findAllElements(elemLst, e, keyElemLst, index + 1);
    }
  }

  /**
   * 引数のキーに最初に該当する下位要素を探索して、存在すればそれを返し、
   * 存在しなければ作成して返す。
   * <br>
   * 引数のキーが不正な場合は、例外をスローする。
   *
   * @param  key キー文字列。
   * @return 引数のキーに最初に対応する、又は作成された要素。
   * @throws IllegalKeyException キーが不正な場合。
   * @throws AssertionError 引数がヌルの場合（デバッグモードのみ）。
   */
  protected Element findOrCreateFirstElement(String key)
    throws IllegalKeyException
  {
    assert (key != null) : "@param:key is null.";

    if (StringOperation.isEmpty(key)) {
      return getBaseElement();
    }

    return findOrCreateFirstElement(splitKey(key));
  }

  /**
   * キー要素リストに最初に該当する下位要素を探索して、存在すればそれを返し、
   * 存在しなければ作成して返す。
   *
   * @param  keyElemLst キー要素リスト。
   * @return 引数のキーに最初に対応する、又は作成された要素。
   * @throws AssertionError 引数がヌルの場合（デバッグモードのみ）。
   */
  protected Element findOrCreateFirstElement(List<String> keyElemLst)
  {
    assert (keyElemLst != null) : "@param:keyElemLst is null.";

    Element elem = getBaseElement();
    List<String> singleKeyL = new ArrayList<String>(1);
    singleKeyL.add("");
    int i = 0;
    for (; i<keyElemLst.size(); i++) {
      singleKeyL.set(0, keyElemLst.get(i));
      Element e = findFirstElement(elem, singleKeyL, 0);
      if (e == null) {
        break;
      }
      elem = e;
    }

    for (; i<keyElemLst.size(); i++) {
      String keyElem = keyElemLst.get(i);
      String elemName = getNameOfKeyElement(keyElem);
      List<String[]> attrLst = listAttributesOfKeyElement(keyElem);

      Element newElem = new Element(elemName);
      for (String[] attr : attrLst) {
        newElem.putAttribute(attr[0], attr[1]);
      }
      elem.addChild(newElem);
      elem = newElem;
    }

    return elem;
  }

  /**
   * 指定されたキーに最初に該当する要素の値文字列を取得する。
   * <br>
   * 該当する要素が一つも存在しない場合は空文字列を返す。
   * 引数のキーが不正な場合は例外をスローする。
   *
   * @param  key キー文字列。
   * @return 引数のキーに最初に該当する要素の値文字列。
   * @throws IllegalKeyException キーが不正な場合。
   * @throws AssertionError 引数がヌルの場合（デバッグモードのみ）。
   */
  public synchronized String getFirstValue(String key)
    throws IllegalKeyException
  {
    Element e = findFirstElement(key);
    if (e == null) {
      return "";
    }

    String s = e.getValue();
    return (s != null) ? s : "";
  }

  /**
   * 指定されたキーに該当する全要素の値文字列のリストを取得する。
   * <br>
   * 該当する要素が一つも存在しない場合は空のリストを返す。
   * 引数のキーが不正な場合は例外をスローする。
   *
   * @param  key キー文字列。
   * @return 引数のキーに該当する全要素の値文字列のリスト。
   * @throws IllegalKeyException キーが不正な場合。
   * @throws AssertionError 引数のヌルの場合（デバッグモードのみ）。
   */
  public synchronized List<String> getValues(String key)
    throws IllegalKeyException
  {
    List<Element> elemLst = findAllElements(key);

    List<String> valueLst = new ArrayList<String>(elemLst.size());
    for (Element elem : elemLst) {
      String s = elem.getValue();
      valueLst.add((s != null) ? s : "");
    }

    return valueLst;
  }

  /**
   * 指定されたキーに最初に該当する要素の、指定された属性の値を取得する。
   * <br>
   * 該当する要素が存在しない場合は空文字列を返す。
   * 該当する要素に、指定された名前の属性が存在しない場合は空文字列を返す。
   * 引数のキーが不正な場合は例外をスローする。
   *
   * @param  key キー文字列。
   * @param  attr 属性名。
   * @return 引数のキーに最初に該当する要素の、指定された属性の値。
   * @throws IllegalKeyException キーが不正な場合。
   * @throws AssertError 引数がヌルの場合。
   */
  public synchronized String getFirstAttribute(String key, String attr)
    throws IllegalKeyException
  {
    assert (attr != null) : "@param:attr is null.";

    Element elem = findFirstElement(key);
    if (elem == null) {
      return "";
    }

    Map<String, String> attrMap = elem.getAttributes();

    String v = attrMap.get(attr);
    if (v == null) {
      return "";
    }

    return v;
  }

  /**
   * 指定されたキーに該当する全要素の、指定された属性の値のリストを取得する。
   * <br>
   * 該当する要素が一つも存在しない場合は空のリストを返す。
   * 引数のキーが不正な場合は例外をスローする。
   *
   * @param  key キー文字列。
   * @param  attr 属性名。
   * @return 引数のキーに該当する全要素の、指定された属性の値のリスト。
   * @throws IllegalKeyException キーが不正な場合。
   * @throws AssertError 引数がヌルの場合。
   */
  public synchronized List<String> getAttributes(String key, String attr)
    throws IllegalKeyException
  {
    assert (attr != null) : "@param:attr is null.";

    List<Element> elemLst = findAllElements(key);

    List<String> valueLst = new ArrayList<String>(elemLst.size());
    for (Element elem : elemLst) {
      AttributeMap attrMap = elem.getAttributes();
      String v = attrMap.get(attr);
      valueLst.add((v != null) ? v : "");
    }

    return valueLst;
  }

  /**
   * 指定されたキーに最初に該当する要素を基底要素とする子リソースを取得する。
   * <br>
   * 指定されたキーに該当する要素が存在しない場合は
   * {@link ts.util.resource.Resource#EMPTY}を返す。
   * 引数のキーが不正な場合は例外をスローする。
   *
   * @param  key キー文字列。
   * @return 引数のキーに最初に該当する要素の子リソース。
   * @throws IllegalKeyException キーが不正な場合。
   * @throws AssertionError 引数がヌルの場合（デバッグモードのみ）。
   */
  public synchronized Resource getFirstChild(String key)
    throws IllegalKeyException
  {
    Element elem = findFirstElement(key);
    if (elem == null) {
      return Resource.EMPTY;
    }

    DefaultResource res = createResource();
    res.baseElement_ = elem;
    return res;
  }

  /**
   * 指定されたキーに最初に該当する子リソースのリストを取得する。
   * <br>
   * 指定されたキーに該当する要素が存在しない場合は空のリストを返す。
   * 引数のキーが不正な場合は例外をスローする。
   *
   * @param  key キー文字列。
   * @return 引数のキーに該当する要素の子リソースのリスト。
   * @throws IllegalKeyException キーが不正な場合。
   * @throws AssertionError 引数がヌルの場合（デバッグモードのみ）。
   */
  public synchronized List<Resource> getChildren(String key)
    throws IllegalKeyException
  {
    List<Element> elemLst = findAllElements(key);

    List<Resource> resLst = new ArrayList<Resource>(elemLst.size());
    for (Element e : elemLst) {
      DefaultResource res = createResource();
      res.baseElement_ = e;
      resLst.add(res);
    }

    return resLst;
  }

  /**
   * 指定されたキーに最初に該当する要素に、値文字列を設定する。
   * <br>
   * 指定されたキーに該当する要素が存在しない場合は、その要素を作成して
   * 値を設定する。
   *
   * @param  key キー文字列。
   * @param  value 引数のキーに最初に該当する要素に設定する値。
   * @throws IllegalKeyException キーが不正な場合。
   * @throws AssertionError 引数がヌルの場合（デバッグモードのみ）。
   */
  public synchronized void setFirstValue(String key, String value)
    throws IllegalKeyException
  {
    assert (value != null) : "@param:value is null.";

    try {
      Element elem = findOrCreateFirstElement(key);
      if (elem == getRootElement()) {
        throw new IllegalKeyException(key);
      }
      elem.setValue(value);
    }
    catch (IllegalKeyException e) {
      throw e;
    }
    catch (Exception e) {
      throw new IllegalKeyException(key, e);
    }
  }

  /**
   * 指定されたキーに最初に該当する要素を基底要素とする子リソースを取得する。
   * 指定されたキーに該当する要素が存在しない場合は作成して返す。
   * <br>
   * 引数のキーが不正な場合は例外をスローする。
   *
   * @param  key キー文字列。
   * @return 指定されたキーに最初に該当する要素を基底要素とする子リソース。
   * @throws IllegalKeyException キーが不正な場合。
   * @throws AssertionError 引数がヌルの場合（デバッグモードのみ）。
   */
  public synchronized Resource getOrCreateFirstChild(String key)
    throws IllegalKeyException
  {
    try {
      Element elem = findOrCreateFirstElement(key);

      DefaultResource res = createResource();
      res.baseElement_ = elem;
      return res;
    }
    catch (IllegalKeyException e) {
      throw e;
    }
  }

  /**
   * 指定されたキーに該当する全ての要素を削除する。
   * <br>
   * 指定されたキーに該当する要素が見つからなかった場合は、何もしない。
   *
   * @param  key キー文字列。
   * @throws IllegalKeyException キーが不正な場合。
   * @throws AssertionError 引数がヌルの場合（デバッグモードのみ）。
   */
  public synchronized void removeChildren(String key) throws IllegalKeyException
  {
    List<Element> elemLst = findAllElements(key);
    for (Element elem : elemLst) {
      Element parent = elem.getParent();
      if (parent != null) {
        parent.removeChildren(elem.getName());
      }
      else {
        renew();
      }
    }
  }
}
