/*
 * Copyright (C) 2011 Whisper Systems
 *
 * This program 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.
 *
 * This program 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 org.thoughtcrime.securesms.directory;

import android.content.Context;
import android.util.Log;

import com.google.thoughtcrimegson.Gson;
import com.google.thoughtcrimegson.JsonParseException;
import com.google.thoughtcrimegson.annotations.SerializedName;
import org.thoughtcrime.securesms.util.PhoneNumberFormatter;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;

/**
 * Handles providing lookups, serializing, and deserializing the RedPhone directory.
 *
 * @author Moxie Marlinspike
 *
 */

public class NumberFilter {

  private static NumberFilter instance;

  public synchronized static NumberFilter getInstance(Context context) {
    if (instance == null)
      instance = NumberFilter.deserializeFromFile(context);

    return instance;
  }

  private static final String DIRECTORY_META_FILE = "directory.stat";

  private File bloomFilter;
  private String version;
  private long capacity;
  private int hashCount;
  private Context context;

  private NumberFilter(Context context, File bloomFilter, long capacity,
                       int hashCount, String version)
  {
    this.context     = context.getApplicationContext();
    this.bloomFilter = bloomFilter;
    this.capacity    = capacity;
    this.hashCount   = hashCount;
    this.version     = version;
  }

  public synchronized boolean containsNumber(String number) {
    try {
      if      (bloomFilter == null)                    return false;
      else if (number == null || number.length() == 0) return false;

      return new BloomFilter(bloomFilter, hashCount).contains(PhoneNumberFormatter.formatNumber(context, number));
    } catch (IOException ioe) {
      Log.w("NumberFilter", ioe);
      return false;
    }
  }

  public synchronized void update(File bloomFilter, long capacity, int hashCount, String version)
  {
    if (this.bloomFilter != null)
      this.bloomFilter.delete();

    this.bloomFilter = bloomFilter;
    this.capacity    = capacity;
    this.hashCount   = hashCount;
    this.version     = version;

    serializeToFile(context);
  }

  private void serializeToFile(Context context) {
    if (this.bloomFilter == null)
      return;

    try {
      FileOutputStream fout       = context.openFileOutput(DIRECTORY_META_FILE, 0);
      NumberFilterStorage storage = new NumberFilterStorage(bloomFilter.getAbsolutePath(),
                                                            capacity, hashCount, version);

      storage.serializeToStream(fout);
      fout.close();
    } catch (IOException ioe) {
      Log.w("NumberFilter", ioe);
    }
  }

  private static NumberFilter deserializeFromFile(Context context) {
    try {
      FileInputStream fis         = context.openFileInput(DIRECTORY_META_FILE);
      NumberFilterStorage storage = NumberFilterStorage.fromStream(fis);

      if (storage == null) return new NumberFilter(context, null, 0, 0, "0");
      else                 return new NumberFilter(context,
                                                   new File(storage.getDataPath()),
                                                   storage.getCapacity(),
                                                   storage.getHashCount(),
                                                   storage.getVersion());
    } catch (IOException ioe) {
      Log.w("NumberFilter", ioe);
      return new NumberFilter(context, null, 0, 0, "0");
    }
  }

  private static class NumberFilterStorage {
    @SerializedName("data_path")
    private String dataPath;

    @SerializedName("capacity")
    private long capacity;

    @SerializedName("hash_count")
    private int hashCount;

    @SerializedName("version")
    private String version;

    public NumberFilterStorage(String dataPath, long capacity, int hashCount, String version) {
      this.dataPath   = dataPath;
      this.capacity   = capacity;
      this.hashCount  = hashCount;
      this.version    = version;
    }

    public String getDataPath() {
      return dataPath;
    }

    public long getCapacity() {
      return capacity;
    }

    public int getHashCount() {
      return hashCount;
    }

    public String getVersion() {
      return version;
    }

    public void serializeToStream(OutputStream out) throws IOException {
      out.write(new Gson().toJson(this).getBytes());
    }

    public static NumberFilterStorage fromStream(InputStream in) throws IOException {
      try {
        return new Gson().fromJson(new BufferedReader(new InputStreamReader(in)),
                                   NumberFilterStorage.class);
      } catch (JsonParseException jpe) {
        Log.w("NumberFilter", jpe);
        throw new IOException("JSON Parse Exception");
      }
    }
  }
}