'use strict';

var defaultRepo = require('./default-repo');
var BlockService = require('ipfs-block-service');
var Block = require('ipfs-block');
var mDAG = require('ipfs-merkle-dag');
var DAGNode = mDAG.DAGNode;
var DAGService = mDAG.DAGService;
var peerId = require('peer-id');
var PeerInfo = require('peer-info');
var multiaddr = require('multiaddr');
var importer = require('ipfs-data-importing').import;
var libp2p = require('libp2p-ipfs');
var init = require('./init');
var IPFSRepo = require('ipfs-repo');

exports = module.exports = IPFS;

function IPFS(repo) {
  var _this = this;

  if (!(this instanceof IPFS)) {
    throw new Error('Must be instantiated with new');
  }

  if (!(repo instanceof IPFSRepo)) {
    repo = defaultRepo(repo);
  }

  var blockS = new BlockService(repo);
  var dagS = new DAGService(blockS);
  var peerInfo;
  var libp2pNode;
  var peerInfoBook = {};

  this.load = function (callback) {
    repo.exists(function (err, exists) {
      if (err) {
        throw err;
      }

      repo.config.get(function (err, config) {
        if (err) {
          throw err;
        }
        var pid = peerId.createFromPrivKey(config.Identity.PrivKey);
        peerInfo = new PeerInfo(pid);
        config.Addresses.Swarm.forEach(function (addr) {
          peerInfo.multiaddr.add(multiaddr(addr));
        });
        callback();
      });
    });
  };

  this.version = function (opts, callback) {
    if (typeof opts === 'function') {
      callback = opts;
      opts = {};
    }

    repo.exists(function (err, exists) {
      if (err) {
        return callback(err);
      }

      repo.config.get(function (err, config) {
        if (err) {
          return callback(err);
        }

        callback(null, config.Version.Current);
      });
    });
  };

  this.id = function (opts, callback) {
    if (typeof opts === 'function') {
      callback = opts;
      opts = {};
    }
    if (!peerInfo) {
      // because of split second warmup
      setTimeout(ready, 100);
    } else {
      ready();
    }
    function ready() {
      callback(null, {
        ID: peerInfo.id.toB58String(),
        PublicKey: peerInfo.id.pubKey.toString('base64'),
        Addresses: peerInfo.multiaddrs.map(function (ma) {
          return ma.toString();
        }),
        AgentVersion: 'js-ipfs',
        ProtocolVersion: '9000'
      });
    }
  };

  this.repo = {
    init: function init(bits, empty, callback) {
      // 1. check if repo already exists
    },

    version: function version(opts, callback) {
      if (typeof opts === 'function') {
        callback = opts;
        opts = {};
      }
      repo.exists(function (err, res) {
        if (err) {
          return callback(err);
        }
        repo.version.get(callback);
      });
    },

    gc: function gc() {},

    path: function path() {
      return repo.path;
    }
  };

  this.init = function (opts, callback) {
    init(repo, opts, callback);
  };

  this.bootstrap = {
    list: function list(callback) {
      repo.config.get(function (err, config) {
        if (err) {
          return callback(err);
        }
        callback(null, config.Bootstrap);
      });
    },
    add: function add(multiaddr, callback) {
      repo.config.get(function (err, config) {
        if (err) {
          return callback(err);
        }
        config.Bootstrap.push(multiaddr);
        repo.config.set(config, function (err) {
          if (err) {
            return callback(err);
          }

          callback();
        });
      });
    },
    rm: function rm(multiaddr, callback) {
      repo.config.get(function (err, config) {
        if (err) {
          return callback(err);
        }
        config.Bootstrap = config.Bootstrap.filter(function (mh) {
          if (mh === multiaddr) {
            return false;
          } else {
            return true;
          }
        });
        repo.config.set(config, function (err) {
          if (err) {
            return callback(err);
          }
          callback();
        });
      });
    }
  };

  this.config = {
    // cli only feature built with show and replace
    // edit: (callback) => {},
    replace: function replace(config, callback) {
      repo.config.set(config, callback);
    },
    show: function show(callback) {
      repo.config.get(function (err, config) {
        if (err) {
          return callback(err);
        }
        callback(null, config);
      });
    }
  };

  this.block = {
    get: function get(multihash, callback) {
      blockS.getBlock(multihash, callback);
    },
    put: function put(block, callback) {
      blockS.addBlock(block, callback);
    },
    del: function del(multihash, callback) {
      blockS.deleteBlock(multihash, callback);
    },
    stat: function stat(multihash, callback) {
      blockS.getBlock(multihash, function (err, block) {
        if (err) {
          return callback(err);
        }
        callback(null, {
          Key: multihash,
          Size: block.data.length
        });
      });
    }
  };

  this.object = {
    new: function _new(template, callback) {
      if (!callback) {
        callback = template;
      }
      var node = new DAGNode();
      var block = new Block(node.marshal());
      blockS.addBlock(block, function (err) {
        if (err) {
          return callback(err);
        }
        callback(null, {
          Hash: block.key,
          Size: node.size(),
          Name: ''
        });
      });
    },
    patch: {
      appendData: function appendData(multihash, data, callback) {
        _this.object.get(multihash, function (err, obj) {
          if (err) {
            return callback(err);
          }
          obj.data = Buffer.concat([obj.data, data]);
          dagS.add(obj, function (err) {
            if (err) {
              return callback(err);
            }
            callback(null, obj);
          });
        });
      },
      addLink: function addLink(multihash, link, callback) {
        _this.object.get(multihash, function (err, obj) {
          if (err) {
            return callback(err);
          }
          obj.addRawLink(link);
          dagS.add(obj, function (err) {
            if (err) {
              return callback(err);
            }
            callback(null, obj);
          });
        });
      },
      rmLink: function rmLink(multihash, linkRef, callback) {
        _this.object.get(multihash, function (err, obj) {
          if (err) {
            return callback(err);
          }
          obj.links = obj.links.filter(function (link) {
            // filter by name when linkRef is a string, or by hash otherwise
            if (typeof linkRef === 'string') {
              return link.name !== linkRef;
            }
            return !link.hash.equals(linkRef);
          });
          dagS.add(obj, function (err) {
            if (err) {
              return callback(err);
            }
            callback(null, obj);
          });
        });
      },
      setData: function setData(multihash, data, callback) {
        _this.object.get(multihash, function (err, obj) {
          if (err) {
            return callback(err);
          }
          obj.data = data;
          dagS.add(obj, function (err) {
            if (err) {
              return callback(err);
            }
            callback(null, obj);
          });
        });
      }
    },
    data: function data(multihash, callback) {
      _this.object.get(multihash, function (err, obj) {
        if (err) {
          return callback(err);
        }
        callback(null, obj.data);
      });
    },
    links: function links(multihash, callback) {
      _this.object.get(multihash, function (err, obj) {
        if (err) {
          return callback(err);
        }
        callback(null, obj.links);
      });
    },
    get: function get(multihash, options, callback) {
      if (typeof options === 'function') {
        callback = options;
        options = {};
      }
      dagS.get(multihash, callback);
    },
    put: function put(dagNode, options, callback) {
      if (typeof options === 'function') {
        callback = options;
        options = {};
      }
      dagS.add(dagNode, callback);
    },
    stat: function stat(multihash, options, callback) {
      if (typeof options === 'function') {
        callback = options;
        options = {};
      }

      _this.object.get(multihash, function (err, obj) {
        if (err) {
          return callback(err);
        }
        var res = {
          NumLinks: obj.links.length,
          BlockSize: obj.marshal().length,
          LinksSize: obj.links.reduce(function (prev, link) {
            return prev + link.size;
          }, 0),
          DataSize: obj.data.length,
          CumulativeSize: ''
        };
        callback(null, res);
      });
    }
  };

  var OFFLINE_ERROR = new Error('This command must be run in online mode. Try running \'ipfs daemon\' first.');

  this.libp2p = {
    start: function start(callback) {
      libp2pNode = new libp2p.Node(peerInfo);
      libp2pNode.start(function () {
        // TODO connect to bootstrap nodes, it will get us more addrs
        peerInfo.multiaddrs.forEach(function (ma) {
          console.log('Swarm listening on', ma.toString());
        });
        callback();
      });
    },
    stop: function stop(callback) {
      libp2pNode.swarm.close(callback);
    },
    swarm: {
      peers: function peers(callback) {
        if (!libp2pNode) {
          return callback(OFFLINE_ERROR);
        }

        callback(null, peerInfoBook);
      },
      // all the addrs we know
      addrs: function addrs(callback) {
        if (!libp2pNode) {
          return callback(OFFLINE_ERROR);
        }
        // TODO
        notImpl();
      },
      localAddrs: function localAddrs(callback) {
        if (!libp2pNode) {
          return callback(OFFLINE_ERROR);
        }

        callback(null, peerInfo.multiaddrs);
      },
      connect: function connect(ma, callback) {
        if (!libp2pNode) {
          return callback(OFFLINE_ERROR);
        }

        var idStr = ma.toString().match(/\/ipfs\/(.*)/);
        if (!idStr) {
          return callback(new Error('invalid multiaddr'));
        }
        var id = peerId.createFromB58String(idStr[1]);
        var peer = new PeerInfo(id);

        ma = ma.toString().replace(/\/ipfs\/(.*)/, ''); // FIXME remove this when multiaddr supports ipfs

        peer.multiaddr.add(multiaddr(ma));
        peerInfoBook[peer.id.toB58String()] = peer;

        libp2pNode.swarm.dial(peer, function (err) {
          callback(err, id);
        });
      },
      disconnect: function disconnect(callback) {
        if (!libp2pNode) {
          return callback(OFFLINE_ERROR);
        }

        notImpl();
      },
      filters: notImpl // TODO
    },
    routing: {},
    records: {},
    ping: notImpl
  };

  this.files = {
    add: function add(path, options, callback) {
      options.path = path;
      options.dagService = dagS;
      importer(options, callback);
    }
  };
}

function notImpl() {
  throw new Error('Not implemented yet');
}