/* Copyright 2012 Mozilla Foundation and Mozilla contributors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

if (!this.ctypes) {
  // We're likely being loaded as a JSM.
  this.EXPORTED_SYMBOLS = [ "libcutils", "libnetutils", "netHelpers" ];
  Components.utils.import("resource://gre/modules/ctypes.jsm");
}

const SYSTEM_PROPERTY_KEY_MAX = 32;
const SYSTEM_PROPERTY_VALUE_MAX = 92;

// We leave this as 'undefined' instead of setting it to 'false'. That
// way a file that includes us can have it defined already without us
// overriding the value here.
let DEBUG;

/**
 * Expose some system-level functions.
 */
this.libcutils = (function() {
  let lib;
  try {
    lib = ctypes.open("libcutils.so");
  } catch(ex) {
    // Return a fallback option in case libcutils.so isn't present (e.g.
    // when building Firefox with MOZ_B2G_RIL.
    if (DEBUG) {
      dump("Could not load libcutils.so. Using fake propdb.\n");
    }
    let fake_propdb = Object.create(null);
    return {
      property_get: function fake_property_get(key, defaultValue) {
        if (key in fake_propdb) {
          return fake_propdb[key];
        }
        return defaultValue === undefined ? null : defaultValue;
      },
      property_set: function fake_property_set(key, value) {
        fake_propdb[key] = value;
      }
    };
  }

  let c_property_get = lib.declare("property_get", ctypes.default_abi,
                                   ctypes.int,       // return value: length
                                   ctypes.char.ptr,  // key
                                   ctypes.char.ptr,  // value
                                   ctypes.char.ptr); // default
  let c_property_set = lib.declare("property_set", ctypes.default_abi,
                                   ctypes.int,       // return value: success
                                   ctypes.char.ptr,  // key
                                   ctypes.char.ptr); // value
  let c_value_buf = ctypes.char.array(SYSTEM_PROPERTY_VALUE_MAX)();

  return {

    /**
     * Get a system property.
     *
     * @param key
     *        Name of the property
     * @param defaultValue [optional]
     *        Default value to return if the property isn't set (default: null)
     */
    property_get: function property_get(key, defaultValue) {
      if (defaultValue === undefined) {
        defaultValue = null;
      }
      c_property_get(key, c_value_buf, defaultValue);
      return c_value_buf.readString();
    },

    /**
     * Set a system property
     *
     * @param key
     *        Name of the property
     * @param value
     *        Value to set the property to.
     */
    property_set: function property_set(key, value) {
      let rv = c_property_set(key, value);
      if (rv) {
        throw Error('libcutils.property_set("' + key + '", "' + value +
                    '") failed with error ' + rv);
      }
    }

  };
})();

/**
 * Network-related functions from libnetutils.
 */
this.libnetutils = (function () {
  let library;
  try {
    library = ctypes.open("libnetutils.so");
  } catch(ex) {
    if (DEBUG) {
      dump("Could not load libnetutils.so!\n");
    }
    // For now we just fake the ctypes library interfacer to return
    // no-op functions when library.declare() is called.
    library = {
      declare: function fake_declare() {
        return function fake_libnetutils_function() {};
      }
    };
  }

  let iface = {
    ifc_enable: library.declare("ifc_enable", ctypes.default_abi,
                                ctypes.int,
                                ctypes.char.ptr),
    ifc_disable: library.declare("ifc_disable", ctypes.default_abi,
                                 ctypes.int,
                                 ctypes.char.ptr),
    ifc_add_host_route: library.declare("ifc_add_host_route",
                                        ctypes.default_abi,
                                        ctypes.int,
                                        ctypes.char.ptr,
                                        ctypes.int),
    ifc_remove_host_routes: library.declare("ifc_remove_host_routes",
                                            ctypes.default_abi,
                                            ctypes.int,
                                            ctypes.char.ptr),
    ifc_set_default_route: library.declare("ifc_set_default_route",
                                           ctypes.default_abi,
                                           ctypes.int,
                                           ctypes.char.ptr,
                                           ctypes.int),
    ifc_get_default_route: library.declare("ifc_get_default_route",
                                           ctypes.default_abi,
                                           ctypes.int,
                                           ctypes.char.ptr),
    ifc_remove_default_route: library.declare("ifc_remove_default_route",
                                              ctypes.default_abi,
                                              ctypes.int,
                                              ctypes.char.ptr),
    ifc_configure: library.declare("ifc_configure", ctypes.default_abi,
                                   ctypes.int,
                                   ctypes.char.ptr,
                                   ctypes.int,
                                   ctypes.int,
                                   ctypes.int,
                                   ctypes.int,
                                   ctypes.int),
    ifc_add_route: library.declare("ifc_add_route", ctypes.default_abi,
                                   ctypes.int, // return value
                                   ctypes.char.ptr, // ifname
                                   ctypes.char.ptr, // dst
                                   ctypes.int, // prefix_length
                                   ctypes.char.ptr), // gw
    ifc_remove_route: library.declare("ifc_remove_route", ctypes.default_abi,
                                      ctypes.int, // return value
                                      ctypes.char.ptr, // ifname
                                      ctypes.char.ptr, // dst
                                      ctypes.int, // prefix_length
                                      ctypes.char.ptr), // gw
    dhcp_stop: library.declare("dhcp_stop", ctypes.default_abi,
                               ctypes.int,
                               ctypes.char.ptr),
    dhcp_release_lease: library.declare("dhcp_release_lease", ctypes.default_abi,
                                        ctypes.int,
                                        ctypes.char.ptr),
    dhcp_get_errmsg: library.declare("dhcp_get_errmsg", ctypes.default_abi,
                                     ctypes.char.ptr),

    // Constants for ifc_reset_connections.
    // NOTE: Ignored in versions before ICS.
    RESET_IPV4_ADDRESSES: 0x01,
    RESET_IPV6_ADDRESSES: 0x02,
  };

  iface.RESET_ALL_ADDRESSES = iface.RESET_IPV4_ADDRESSES |
                              iface.RESET_IPV6_ADDRESSES

  // dhcp_do_request's interface changed in SDK version 15. We try to hide
  // this here by implementing the same JS API for both versions.

  let sdkVersion = libcutils.property_get("ro.build.version.sdk") || "0";
  sdkVersion = parseInt(sdkVersion, 10);
  if (sdkVersion >= 15) {
    let ipaddrbuf = ctypes.char.array(4096)();
    let gatewaybuf = ctypes.char.array(4096)();
    let prefixLen = ctypes.int();
    let dns1buf = ctypes.char.array(4096)();
    let dns2buf = ctypes.char.array(4096)();
    let serverbuf = ctypes.char.array(4096)();
    let lease = ctypes.int();
    let vendorbuf = ctypes.char.array(4096)();
    let c_dhcp_do_request;

    // also changed for 16
    if (sdkVersion >= 16) {
      c_dhcp_do_request =
        library.declare("dhcp_do_request", ctypes.default_abi,
                        ctypes.int,       // return value
                        ctypes.char.ptr,  // ifname
                        ctypes.char.ptr,  // ipaddr
                        ctypes.char.ptr,  // gateway
                        ctypes.int.ptr,   // prefixlen
                        ctypes.char.ptr,  // dns1
                        ctypes.char.ptr,  // dns2
                        ctypes.char.ptr,  // server
                        ctypes.int.ptr,   // lease
                        ctypes.char.ptr); // vendorinfo
    } else {
      c_dhcp_do_request =
        library.declare("dhcp_do_request", ctypes.default_abi,
                        ctypes.int,      // return value
                        ctypes.char.ptr, // ifname
                        ctypes.char.ptr, // ipaddr
                        ctypes.char.ptr, // gateway
                        ctypes.int.ptr,  // prefixlen
                        ctypes.char.ptr, // dns1
                        ctypes.char.ptr, // dns2
                        ctypes.char.ptr, // server
                        ctypes.int.ptr); // lease
    }


    iface.dhcp_do_request = function dhcp_do_request(ifname) {
      let ret;
      if (sdkVersion >= 16) {
        ret = c_dhcp_do_request(ifname,
                                ipaddrbuf,
                                gatewaybuf,
                                prefixLen.address(),
                                dns1buf,
                                dns2buf,
                                serverbuf,
                                lease.address(),
                                vendorbuf);
      } else {
        ret = c_dhcp_do_request(ifname,
                                ipaddrbuf,
                                gatewaybuf,
                                prefixLen.address(),
                                dns1buf,
                                dns2buf,
                                serverbuf,
                                lease.address());
      }

      if (ret && DEBUG) {
        let error = iface.dhcp_get_errmsg();
        dump("dhcp_do_request failed - " + error.readString());
      }
      let obj = {
        ret: ret | 0,
        ipaddr_str: ipaddrbuf.readString(),
        mask: netHelpers.makeMask(prefixLen.value),
        gateway_str: gatewaybuf.readString(),
        dns1_str: dns1buf.readString(),
        dns2_str: dns2buf.readString(),
        server_str: serverbuf.readString(),
        lease: lease.value | 0,
        vendor_str: vendorbuf.readString()
      };
      obj.ipaddr = netHelpers.stringToIP(obj.ipaddr_str);
      obj.mask_str = netHelpers.ipToString(obj.mask);
      obj.broadcast_str = netHelpers.ipToString((obj.ipaddr & obj.mask) + ~obj.mask);
      obj.gateway = netHelpers.stringToIP(obj.gateway_str);
      obj.dns1 = netHelpers.stringToIP(obj.dns1_str);
      obj.dns2 = netHelpers.stringToIP(obj.dns2_str);
      obj.server = netHelpers.stringToIP(obj.server_str);
      return obj;
    };
    // dhcp_do_request_renew() went away in newer libnetutils.
    iface.dhcp_do_request_renew = iface.dhcp_do_request;

    // Same deal with ifc_reset_connections.
    let c_ifc_reset_connections =
      library.declare("ifc_reset_connections",
                      ctypes.default_abi,
                      ctypes.int,
                      ctypes.char.ptr,
                      ctypes.int);
    iface.ifc_reset_connections = function(ifname, reset_mask) {
      return c_ifc_reset_connections(ifname, reset_mask) | 0;
    }
  } else {
    let ints = ctypes.int.array(8)();
    let c_dhcp_do_request =
      library.declare("dhcp_do_request", ctypes.default_abi,
                      ctypes.int,      // return value
                      ctypes.char.ptr, // ifname
                      ctypes.int.ptr,  // ipaddr
                      ctypes.int.ptr,  // gateway
                      ctypes.int.ptr,  // mask
                      ctypes.int.ptr,  // dns1
                      ctypes.int.ptr,  // dns2
                      ctypes.int.ptr,  // server
                      ctypes.int.ptr); // lease
    let c_dhcp_do_request_renew =
      library.declare("dhcp_do_request_renew", ctypes.default_abi,
                      ctypes.int,      // return value
                      ctypes.char.ptr, // ifname
                      ctypes.int.ptr,  // ipaddr
                      ctypes.int.ptr,  // gateway
                      ctypes.int.ptr,  // mask
                      ctypes.int.ptr,  // dns1
                      ctypes.int.ptr,  // dns2
                      ctypes.int.ptr,  // server
                      ctypes.int.ptr); // lease

    let wrapCFunc = function wrapCFunc(c_fn) {
      return function (ifname) {
        let ret = c_fn(ifname,
                       ints.addressOfElement(0),
                       ints.addressOfElement(1),
                       ints.addressOfElement(2),
                       ints.addressOfElement(3),
                       ints.addressOfElement(4),
                       ints.addressOfElement(5),
                       ints.addressOfElement(6));
        if (ret && DEBUG) {
          let error = iface.dhcp_get_errmsg();
          dump("dhcp_do_request_* failed - " + error.readString());
        }
        return {ret: ret | 0,
                ipaddr: ints[0] | 0,
                gateway: ints[1] | 0,
                mask: ints[2] | 0,
                dns1: ints[3] | 0,
                dns2: ints[4] | 0,
                server: ints[5] | 0,
                lease: ints[6] | 0};
      };
    };
    iface.dhcp_do_request = wrapCFunc(c_dhcp_do_request);
    iface.dhcp_do_request_renew = wrapCFunc(c_dhcp_do_request_renew);
    let c_ifc_reset_connections =
      library.declare("ifc_reset_connections",
                      ctypes.default_abi,
                      ctypes.int,
                      ctypes.char.ptr);
    iface.ifc_reset_connections = function(ifname, reset_mask) {
      return c_ifc_reset_connections(ifname) | 0;
    }
  }

  return iface;
})();

/**
 * Helpers for conversions.
 */
this.netHelpers = {

  /**
   * Swap byte orders for 32-bit value
   */
  swap32: function swap32(n) {
    return (((n >> 24) & 0xFF) <<  0) |
           (((n >> 16) & 0xFF) <<  8) |
           (((n >>  8) & 0xFF) << 16) |
           (((n >>  0) & 0xFF) << 24);
  },

  /**
   * Convert network byte order to host byte order
   * Note: Assume that the system is little endian
   */
  ntohl: function ntohl(n) {
    return this.swap32(n);
  },

  /**
   * Convert host byte order to network byte order
   * Note: Assume that the system is little endian
   */
  htonl: function htonl(n) {
    return this.swap32(n);
  },

  /**
   * Convert integer representation of an IP address to the string
   * representation.
   *
   * @param ip
   *        IP address in number format.
   */
  ipToString: function ipToString(ip) {
    return ((ip >>  0) & 0xFF) + "." +
           ((ip >>  8) & 0xFF) + "." +
           ((ip >> 16) & 0xFF) + "." +
           ((ip >> 24) & 0xFF);
  },

  /**
   * Convert string representation of an IP address to the integer
   * representation.
   *
   * @param string
   *        String containing the IP address.
   */
  stringToIP: function stringToIP(string) {
    let ip = 0;
    let start, end = -1;
    for (let i = 0; i < 4; i++) {
      start = end + 1;
      end = string.indexOf(".", start);
      if (end == -1) {
        end = string.length;
      }
      let num = parseInt(string.slice(start, end), 10);
      if (isNaN(num)) {
        return null;
      }
      ip |= num << (i * 8);
    }
    return ip;
  },

  /**
   * Make a subnet mask.
   */
  makeMask: function makeMask(len) {
    let mask = 0;
    for (let i = 0; i < len; ++i) {
      mask |= (0x80000000 >> i);
    }
    return this.ntohl(mask);
  },

  /**
   * Get Mask length from given mask address
   */
  getMaskLength: function getMaskLength(mask) {
    let len = 0;
    let netmask = this.ntohl(mask);
    while (netmask & 0x80000000) {
        len++;
        netmask = netmask << 1;
    }
    return len;
  }
};
