package jp.co.y_net.amm.dao;


import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.lang.reflect.Field;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

import javax.servlet.ServletContext;

import org.apache.wicket.protocol.http.WebApplication;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

/**
* テキストファイルをストレージとするData Access Object
*
* オブジェクトはJSON形式に変換する。
*
* 必須ライブラリは、親プロジェクトの pom.xml に記述
* 
        <!-- Jackson -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-core</artifactId>
            <version>2.5.0</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.5.0</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-annotations</artifactId>
            <version>2.5.0</version>
        </dependency>
*
* @author k.inaba
*/
public class TextDao {

     /*
     * --------------------------------------------------------------------------
     * サンプル実装
     */

     /**
     * 実装のサンプル このDAOの使用方法を示す。
     * @param args パラメータ不要
     */
     public static void main(String[] args) {

          /*
          * DAOをインスタンス化する。
          * このとき保存先を指定する。
          */
          TextDao dao = new TextDao("C:\\tmp\\textdb");

          /* 登録のサンプル */

          TextDBAccSampleDto insertdto1 = new TextDBAccSampleDto(10, "aiueo");
          int insertId1 = dao.insert(insertdto1);
          System.out.println(insertdto1.toString() + "を[" + insertId1 + "]として保存しました。");

          TextDBAccSampleDto insertdto2 = new TextDBAccSampleDto(10, "あいうえお");
          int insertId2 = dao.insert(insertdto2);
          System.out.println(insertdto2.toString() + "を[" + insertId2 + "]として保存しました。");

          /* 検索のサンプル*/

          TextDBAccSampleDto condA = new TextDBAccSampleDto();
          condA.col1 = 10;
          List<TextDBAccSampleDto> resultsA = dao.select(condA, TextDBAccSampleDto.class);

          System.out.println(condA.toString() + " を条件として検索を実行。");
          for (TextDBAccSampleDto dto: resultsA) {
               System.out.println(">" + dto.toString());
          }

          TextDBAccSampleDto condB = new TextDBAccSampleDto();
          condB.col1 = 10;
          condB.col2 = "あいうえお";
          List<TextDBAccSampleDto> resultsB = dao.select(condB, TextDBAccSampleDto.class);

          System.out.println(condB.toString() + " を条件として検索を実行。");
          for (TextDBAccSampleDto dto: resultsB) {
               System.out.println(">" + dto.toString());
          }

          /* 削除のサンプル */

          List<TextDBAccSampleDto> resultsAll = dao.select(TextDBAccSampleDto.class);
          Collections.reverse(resultsAll);
          int i = 0;
          for (TextDBAccSampleDto dto : resultsAll) {
               System.out.println(dto.toString());
               if (i > 5) {
                    System.out.println(dto.id + " を削除します。");
                    // dao.delete(r, false); 論理削除ではなく、実際にファイルを削除する。
                    dao.delete(dto);
               }
               i++;
          }
     }

     /**
     * 実装のサンプル このDAOで扱うDTOを実装する際の参考とする。
     */
     private static class TextDBAccSampleDto {
          /**
          * 制御用のフィールド[id]
          *
          */
          public int id;
          /* 保存するフィールド */
          public int col1;
          public String col2;

          /**
          * JSONからオブジェクトへ変換するために、 引数付きのコンストラクタが実装されている場合は、 デフォルトコンストラクタの実装が必要である。
          */
          public TextDBAccSampleDto() {
               // 処理なし
          }

          public TextDBAccSampleDto(int col1, String col2) {
               this.col1 = col1;
               this.col2 = col2;
          }

          @Override
          public String toString() {
               return "TextDBAccSampleBean [id=" + id + ", col1=" + col1
                         + ", col2=" + col2 + "]";
          }
     }

     /* -------------------------------------------------------------------------- */

     private File dbroot;
     /**
     * @param root データを保存するディレクトリ
     */
     public TextDao(String dbname) {
          dbroot = new File(getDbPath(dbname));
          if (dbroot.exists() == false) {
               dbroot.mkdirs();
          }
     }
     /**
      * Tomcatインスタンスのコンテキストルートより２階層上位のディレクトリを
      * テキストデータを保存するディレクトリのパスとして取得する。
      * 
      * Linuxでの実行環境では
      * 　/var/local/opt/tomcat/apache-tomcat/7.0/{???}-web/{dbname}/
      * 
      * 開発環境(ローカルのEclipse)では
      * 　{ワークスペース}\.metadata\.plugins\org.eclipse.wst.server.core\tmp0\{dbname}
      * 
      * @return テキストデータを保存するディレクトリのパス
      */
     private static String getDbPath(String dbname) {
         ServletContext servletContext = WebApplication.get().getServletContext();
         File tomCatInstance = new File(servletContext.getRealPath("")).getParentFile().getParentFile(); // コンテキストルートより２階層上位
         File file = new File(new File(tomCatInstance, dbname), dbname + ".txt");
         if (file.exists() == false) {
             file.getParentFile().mkdirs();
             try {
                 boolean result = file.createNewFile();
                 if (result == false) {
                     throw new RuntimeException("ファイルの作成に失敗：" + file.getAbsolutePath());
                 }
             } catch (IOException e) {
                 throw new RuntimeException(e);
             }
         }
         return file.getParent();
     }

     /**
     * 保存する
     * @param bean 保存するデータ
     * @return id 保存したときに付与されたid
     */
     public int insert(Object bean) {
          String tablename = bean.getClass().getName();
          int maxId = -1;
          File[] existfiles = getDbFiles(tablename);
          for (File existfile : existfiles) {
               int id = toId(existfile.getName(), tablename);
               maxId = Math.max(maxId, id);
          }
          int insertId = maxId + 1;
          ObjectMapper objectMapper = new ObjectMapper();
          try {
               String json = objectMapper.writeValueAsString(bean);
               File f = toFile(bean.getClass(), (insertId));
               writeText(f.getAbsolutePath(), json);
          } catch (JsonProcessingException e) {
               throw new RuntimeException(e);
          }
          return insertId;
     }

     /**
     * 指定されたクラスのデータをすべて取得する。
     * @param clz 指定クラス
     * @return 取得したデータ
     */
     public <T> List<T> select(Class<T> clz) {
          return select(null, clz) ;
     }

     /**
     * 指定されたクラスのデータのうち、引数 cond の条件に合うものを取得する。
     *
     * ・cond のフィールドのうち、 nullでないフィールドが検索条件となる。
     * ・AND条件となる。
     * ・フィールド id は無視する。
     *
     * @param clz 指定クラス
     * @return 取得したデータ
     */
     public <T> List<T> select(T cond, Class<T> clz) {
          List<Field> cndFields = new ArrayList<Field>();
          for (Field f : clz.getFields()) {
               if (f.getName().equals("id") == false) {
                    cndFields.add(f);
               }
          }
          String tablename = clz.getName();
          File[] existfiles = getDbFiles(tablename);
          List<T> result = new ArrayList<T>();
          for (File file : existfiles) {
               // String text = "{\"a\":123,\"b\":true,\"c\":\"あいう\"}";
               String text = readTextAsString(file.getAbsolutePath());
               ObjectMapper mapper = new ObjectMapper();
               T bean;
               try {
                    // JsonNode root = mapper.readTree(text);
                    // String c = root.get("c").asText();
                    bean = mapper.readValue(text, clz);
               } catch (JsonProcessingException e) {
                    throw new RuntimeException(e);
               } catch (IOException e) {
                    throw new RuntimeException(e);
               }

               /* 絞り込みのための判定 */
               boolean isMatch;
               if (cond == null) {
                    /* 条件なしのため必ず選択(true) */
                    isMatch = true;
               } else {
                    isMatch = true;
                    for (Field cndField : cndFields) {
                         Object cndValue;
                         Object targetValue;
                         try {
                              cndValue = cndField.get(cond);
                              targetValue = cndField.get(bean);
                         } catch (Exception e) {
                              throw new RuntimeException(e);
                         }
                         if (cndValue != null
                                   && cndValue.equals(targetValue) == false) {
                              /* 不一致の条件を検出 */
                              isMatch = false;
                              break;
                         }
                    }
               }
               if (isMatch) {
                    /* beanにidを付与する */
                    /* ファイル名からidを取得する */
                    try {
                         Field f = clz.getField("id");
                         f.set(bean, toId(file.getName(), tablename));
                    } catch (NoSuchFieldException e) {
                         /* フィールド[id]がなければ無視する。エラーとしない。 */
                    } catch (Exception e) {
                         throw new RuntimeException(e);
                    }
                    result.add(bean);
               }
          }
          return result;
     }

     /**
     * 指定されたBeanのidと一致するデータを論理削除する。
     * 論理削除の場合は、該当のデータの拡張子に[.del]が付与され、無効化される。
     * @param bean
     */
     public void delete(Object bean) {
          delete(bean, true); // デフォルトは論理削除
     }
     /**
     * 指定されたBeanのidと一致するデータを削除する。
     * 論理削除の場合は、該当のデータの拡張子に[.del]が付与され、無効化される。
     * @param bean
     * @param logical 論理削除とする場合はTrue, 物理削除はFalseを指定する。
     */
     private void delete(Object bean, boolean logical) {
          int id;
          try {
               Field f = bean.getClass().getField("id");
               id = f.getInt(bean);
          } catch (NoSuchFieldException e) {
               throw new RuntimeException("フィールド[id]がないbeanは削除できません。", e);
          } catch (Exception e) {
               throw new RuntimeException(e);
          }
          File file = toFile(bean.getClass(), id);
          if (logical) {
               /* 論理削除 （ファイル名の末尾に[.del]を付与する） */
               File dest = getDelFile(file);
               file.renameTo(dest);
          } else {
               file.delete();
          }
     }

     /**
     * 引数srcのファイルオブジェクトのファイル名の末尾に [.del]を付与したオブジェクトを取得する。
     * すでに同名のファイルが存在するときは、[.del.del]となるように 再起処理を行う。
     *
     * @param src
     * @return
     */
     private File getDelFile(File src) {
          File delFile = new File(src.getAbsolutePath() + ".del");
          if (delFile.exists()) {
               return getDelFile(delFile);
          }
          return delFile;
     }

     private File toFile(Class<? extends Object> clz, int id) {
          String tablename = clz.getName();
          String filename = dbroot.getAbsolutePath() + File.separatorChar
                    + tablename + "_" + id + ".txt";
          return new File(filename);
     }

     private int toId(String filename, String tablename) {
          // Matcher m = Pattern.compile("_([0-9]+)\\.txt").matcher(tmp);
          // tmp = m.group(1);
          String tmp = filename;
          tmp = tmp.substring(tablename.length()); // 接頭のテーブル名を除去
          tmp = tmp.substring(1); // _を除去
          tmp = tmp.substring(0, tmp.lastIndexOf(".txt"));
          int id;
          try {
               id = Integer.parseInt(tmp);
          } catch (NumberFormatException e) {
               throw new RuntimeException(e);
          }
          return id;
     }

     private File[] getDbFiles(final String tablename) {
          File[] existfiles = dbroot.listFiles(new FileFilter() {
               @Override
               public boolean accept(File f) {
                    return (f.getName().startsWith(tablename + "_") && f.getName()
                              .endsWith(".txt"));
               }
          });
          return existfiles;
     }
     /*------------------------------------------------------------------------------*/
     private static final String DEFAULT_ENC = "UTF-8";
     public static final String FILE_SEPARATOR = File.separator;
     public static final String LINE_SEPARATOR = System.getProperty("line.separator");
     /**
      * テキストファイルを書き出す
      * @param filepath
      * @param charsetName 例:"UTF-8"
      * @param text
      */
     public static void writeText(String filepath, String charsetName, List<String> text) {
         File f = new File(filepath);
         OutputStream os = null;
         Writer w = null;
         BufferedWriter bw = null;
         try {
             os = new FileOutputStream(f);
             w  = new OutputStreamWriter(os, charsetName);
             bw = new BufferedWriter(w);
             for (String line: text) {
                 bw.write(line);
                 bw.newLine();
                 //bw.flush();
             }
         } catch (Exception e) {
             throw new RuntimeException(e);
         } finally {
             try {
                 if (bw != null) bw.close();
                 if (w != null) w.close();
                 if (os != null) os.close();
             } catch (IOException e) {
                 throw new RuntimeException(e);
             }
         }
     }
     public static void writeText(String filepath, String charsetName, String text) {
         String[] array = text.split(LINE_SEPARATOR);
         List<String> list = Arrays.asList(array);
         writeText(filepath, charsetName, list);
     }
     public static void writeText(String filepath, List<String> string) {
         writeText(filepath, DEFAULT_ENC, string);
     }
     public static void writeText(String filepath, String string) {
         writeText(filepath, DEFAULT_ENC, string);
     }
     public static String readTextAsString(String filepath, String charsetName) {
         StringBuilder sb = new StringBuilder();
         List<String> lines = readText(filepath, charsetName);
         for (String line: lines) {
             sb.append(line).append(LINE_SEPARATOR);
         }
         return sb.toString();
     }
     /**
      * テキストファイルを読み込む
      * @param filepath
      * @return
      */
     public static String readTextAsString(String filepath) {
         return readTextAsString(filepath, DEFAULT_ENC);
     }
     /**
      * テキストファイルを読み込む
      * @param filepath
      * @param charsetName 例:"UTF-8"
      * @return List<String>
      */
     public static List<String> readText(String filepath, String charsetName) {
         if( new File(filepath).exists() == false ) {
             throw new RuntimeException("ファイルが見つかりません。 [" + new File(filepath).getAbsolutePath() + "]");
         }
         List<String> result = new ArrayList<String>();
         FileInputStream is = null;
         InputStreamReader r = null;
         BufferedReader br = null;
         try {
             is = new FileInputStream(filepath);
             r = new InputStreamReader(is, Charset.forName(charsetName));
             br = new BufferedReader(r);
//           int ch;
//           while ((ch = in.read()) != -1) {
             String line;
             while((line = br.readLine()) != null) {
                 result.add(line);

                 //System.out.print(str + NEW_LINE_CODE);
                 /* Unicode表現でデバッグ表示 */
                 //System.out.print(Integer.toHexString(ch) + " ");
             }
         } catch (IOException e) {
             throw new RuntimeException(e);
         } finally {
             try {
                 if (br != null) br.close();
                 if (r != null) r.close();
                 if (is != null) is.close();
             } catch (IOException e) {
                 throw new RuntimeException(e);
             }
         }
         return result;
     }
}