# Copyright 2012 Viewfinder Inc. All Rights Reserved.

"""Viewfinder asset id prefixes and helpers.
"""

__author__ = 'andy@emailscrubbed.com (Andy Kimball)'

import struct

from collections import namedtuple
from tornado import gen
from viewfinder.backend.base import base64hex, util
from viewfinder.backend.base.exceptions import InvalidRequestError, PermissionError
from viewfinder.backend.db.device import Device


class IdPrefix(object):
  """An asset-id is base-64 hex encoded and then prefixed with a single
  char that identifies the type of id. The prefix must be unique and
  listed below.
  """
  Activity = 'a'
  Comment = 'c'
  Episode = 'e'
  Operation = 'o'
  Photo = 'p'
  Post = 't'
  Viewpoint = 'v'

  @staticmethod
  def GetAssetName(prefix):
    """Return name of the asset that has the specified prefix."""
    if not hasattr(IdPrefix, '_prefixes'):
      IdPrefix._prefixes = {}
      for slot in dir(IdPrefix):
        if not slot.startswith('_'):
          prefix = getattr(IdPrefix, slot)
          if isinstance(prefix, str):
            assert prefix not in IdPrefix._prefixes
            IdPrefix._prefixes[prefix] = slot

    return IdPrefix._prefixes.get(prefix, None)

  @staticmethod
  def IsValid(prefix):
    """Return true if "prefix" is a uniquely defined id prefix."""
    return IdPrefix.GetAssetName(prefix) is not None


AssetIdUniquifier = namedtuple('AssetIdUniquifier', ['client_id', 'server_id'])
"""All asset ids must be globally unique, even across viewpoints or users.
All asset ids contain a device id and a unique numeric id generated by
that device (i.e. the client_id). If this is enough to guarantee
uniqueness, then the "server_id" field will be None. However, if the
server generates an asset, it may need to specify a server-derived byte
string id (i.e. the server_id) in order to provide the required uniqueness.
"""


def ConstructAssetId(id_prefix, device_id, uniquifier):
  """Constructs an asset id that does not have a timestamp part. The
  asset id is base-64 hex encoded so that it sorts the same way as
  its binary representation and can be safely included in URLs. The
  "id_prefix" is appended to the resulting id. The binary format of
  the asset id is as follows:

  device_id (var-length numeric): id of the generating device
  client_id (var-length numeric): unique id generated by the device
  server_id (byte str): optionally generated by the server
  """
  assert IdPrefix.IsValid(id_prefix), id_prefix

  # Encode the device_id.
  byte_str = util.EncodeVarLengthNumber(device_id)

  # Append the encoded asset-id uniquifier.
  byte_str += _EncodeUniquifier(uniquifier)

  # Base64-hex encode the bytes to preserve ordering while attaining URL-inclusion safety.
  return id_prefix + base64hex.B64HexEncode(byte_str, padding=False)


def ConstructTimestampAssetId(id_prefix, timestamp, device_id, uniquifier, reverse_ts=True):
  """Constructs an asset id that has a leading 4-byte encoded timestamp,
  which may be reversed. The asset id is base-64 hex encoded so that it
  sorts the same way as its binary representation and can be safely
  included in URLs. The "id_prefix" is appended to the resulting id.
  The binary format of the asset id is as follows:

  timestamp (32 bits): whole seconds since Unix epoch
  device_id (var-length numeric): id of the generating device
  client_id (var-length numeric): unique id generated by the device
  server_id (byte str): optionally generated by the server
  """
  assert IdPrefix.IsValid(id_prefix), id_prefix

  # Drop fractional seconds and possibly reverse the timestamp before converting to raw bytes.
  assert timestamp < 1L << 32, timestamp
  if reverse_ts:
    timestamp = (1L << 32) - int(timestamp) - 1
  byte_str = struct.pack('>I', timestamp)
  assert len(byte_str) == 4, timestamp

  # Append the encoded device_id.
  byte_str += util.EncodeVarLengthNumber(device_id)

  # Append the encoded asset-id uniquifier.
  byte_str += _EncodeUniquifier(uniquifier)

  # Base64-hex encode the bytes for URL-inclusion safety.
  return id_prefix + base64hex.B64HexEncode(byte_str, padding=False)


def DeconstructAssetId(id_prefix, asset_id):
  """Deconstructs an asset id that was previously constructed according
  to the rules of "ConstructAssetId" (i.e. no timestamp). Returns a tuple:
    (device_id, uniquifier)
  """
  assert IdPrefix.IsValid(id_prefix), id_prefix
  assert asset_id[0] == id_prefix, asset_id

  # Decode the bytes, which must be base-64 hex encoded.
  byte_str = base64hex.B64HexDecode(asset_id[1:], padding=False)

  # Decode the device_id and the uniquifier.
  device_id, num_bytes = util.DecodeVarLengthNumber(byte_str)
  uniquifier = _DecodeUniquifier(byte_str[num_bytes:])

  # Return all parts as a tuple.
  return device_id, uniquifier


def DeconstructTimestampAssetId(id_prefix, asset_id, reverse_ts=True):
  """Deconstructs an asset id that was previously constructed according
  to the rules of "ConstructTimestampAssetId" (i.e. includes timestamp).
  Returns a tuple:
    (timestamp, device_id, uniquifier)
  """
  assert IdPrefix.IsValid(id_prefix), id_prefix
  assert asset_id[0] == id_prefix, asset_id

  # Decode the bytes, which must be base-64 hex encoded.
  byte_str = base64hex.B64HexDecode(asset_id[1:], padding=False)

  # Decode the 4-byte timestamp and reverse it if requested.
  timestamp, = struct.unpack('>I', byte_str[:4])
  if reverse_ts:
    timestamp = (1L << 32) - timestamp - 1

  # Decode the device_id and the uniquifier.
  device_id, num_bytes = util.DecodeVarLengthNumber(byte_str[4:])
  uniquifier = _DecodeUniquifier(byte_str[4 + num_bytes:])

  # Return all parts as a tuple.
  return timestamp, device_id, uniquifier


@gen.coroutine
def VerifyAssetId(client, user_id, device_id, prefix_id, asset_id, has_timestamp):
  """Verifies that "asset_id" conforms to the following requirements:
     1. The asset prefix must match "prefix_id" and the asset id's format must be valid.
     2. The embedded device_id must match "device_id", or must match another device owned by
        "user_id". A device can only create assets with ids that match itself.
     3. The asset_id's uniquifier cannot include a server_id part. Only the server can create
        uniquifiers with this part.
  """
  try:
    asset_name = IdPrefix.GetAssetName(prefix_id).lower()
    if has_timestamp:
      truncated_ts, embedded_device_id, uniquifier = DeconstructTimestampAssetId(prefix_id, asset_id)
    else:
      embedded_device_id, uniquifier = DeconstructAssetId(prefix_id, asset_id)
  except:
    raise InvalidRequestError('%s id "%s" does not have a valid format.' %
                              (asset_name, asset_id))

  if embedded_device_id != device_id:
    # Query the database to see if the client owns the embedded device id.
    device = yield gen.Task(Device.Query, client, user_id, embedded_device_id, None, must_exist=False)
    if device is None:
      raise PermissionError('User %d and device %d do not have permission to create %s "%s".' %
                            (user_id, device_id, asset_name, asset_id))

  if uniquifier.server_id is not None:
    raise PermissionError('Clients do not have permission to create %s "%s".' %
                          (asset_name, asset_id))


def _EncodeUniquifier(uniquifier):
  """If "uniquifier" is an int or long, then assumes that there is no
  server_id component needed to make the asset id unique. Otherwise,
  expects "uniquifier" to be a tuple containing (client_id,
  server_id).

  Encodes the client_id and server_id as a combined byte str and returns
  it.
  """
  if type(uniquifier) in (int, long):
    byte_str = util.EncodeVarLengthNumber(uniquifier)
  else:
    client_id, server_id = uniquifier
    assert server_id is None or type(server_id) in (str, unicode), (server_id, type(server_id))

    byte_str = util.EncodeVarLengthNumber(client_id)

    if server_id is not None:
      byte_str += str(server_id)

  return byte_str


def _DecodeUniquifier(byte_str):
  """Decodes the byte str produced by "_EncodeUniquifier" and returns
  the component parts as an AssetIdUniquifier tuple.
  """
  client_id, num_bytes = util.DecodeVarLengthNumber(byte_str)
  server_id = byte_str[num_bytes:] if num_bytes < len(byte_str) else None
  return AssetIdUniquifier(client_id, server_id)
