/*
 * maskmap - Tcl extension for address mask map data structures
 * Copyright 2006-2012 Ian Jackson
 *
 * 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 2 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 library; if not, see <http://www.gnu.org/licenses/>.
 */

#include "chiark_tcl_hbytes.h"

/*---------- operations on AddrMap_Entry ----------*/

static void ame_init(AddrMap_Entry *ame) {
  ame->prefixlen= -1;
  ame->prefix= 0;
  ame->data= 0;
}

static unsigned ame_clear_unwanted(AddrMap_Entry *ame, int bytes) {
  /* returns non-0 iff some bits were cleared */
  int sparebits;
  unsigned result, sparemask;
  Byte *datap;

  sparebits= bytes * 8 - ame->prefixlen;
  if (!sparebits) return 0;

  sparemask= (1u << sparebits) - 1;
  datap= &ame->prefix[bytes-1];

  result= *datap & sparemask;
  *datap &= ~sparemask;

  return result;
}

static int ame_parsekey(Tcl_Interp *ip, AddrMap_Entry *ame,
			Tcl_Obj *prefixo, Tcl_Obj *prefixbitso,
			int inmap) {
 /* *ame should be blank entry; after exit (even error exit) it will be valid
  * - on errors, it will be blank.  inmap is 1 if we're parsing an existing
  * map or 0 if it's an entry to be added or modified. */
  HBytes_Value prefix;
  int suppliedprefixbytes, prefixbits, wantprefixbytes;
  const Byte *data;
  int rc;

  hbytes_empty(&prefix);
  
  rc= pat_hb(ip,prefixo,&prefix);  if (rc) goto x_rc;
  rc= pat_int(ip,prefixbitso,&prefixbits);  if (rc) goto x_rc;

  wantprefixbytes= prefix_bytes(prefixbits);
  suppliedprefixbytes= hbytes_len(&prefix);

  if (suppliedprefixbytes < wantprefixbytes) {
    rc= staticerr(ip, "addr-map entry PREFIX too short for PREFIX-LEN",
		  "HBYTES ADDRMAP SYNTAX UNDERRUN");
    goto x_rc;
  }
  if (inmap && suppliedprefixbytes > wantprefixbytes) {
    rc= staticerr(ip, "addr-map existing entry PREFIX too long for PREFIX-LEN",
		  "HBYTES ADDRMAP SYNTAX OVERRUN");
    goto x_rc;
  }

  ame->prefixlen= prefixbits;
  ame->prefix= TALLOC(wantprefixbytes);  assert(ame->prefix);
  memcpy(ame->prefix, data, wantprefixbytes);

  if (ame_clear_unwanted(ame, wantprefixbytes)) {
    rc= staticerr(ip, "addr-map entry PREFIX contains bits excluded"
		  " by PREFIX-LEN", "HBYTES ADDRMAP SYNTAX EXCLBITS");
    goto x_rc;
  }

  return TCL_OK;

 x_rc:
  ame_free(ame);
  return rc;
}

static int ame_contains(const AddrMap_Entry *ref, const Byte *addr, int len) {
  int directbytes, leftoverbits;
  
  assert(len >= ref->prefixlen);
  
  directbytes= ref->prefixlen / 8;
  if (memcmp(ref->prefix, addr, directbytes)) return 0;

  leftoverbits= ref->prefixlen % 8;
  if (leftoverbits)
    if ((addr[directbytes] & (0xffu << leftoverbits))
	!= search->prefix[directbytes])
      return 0;

  return 1;
}  

static int ame_compare(const AddrMap_Entry *a, const AddrMap_Entry *b) {
  /*    +2 =    a   covers later range of address space than  b
   *    +1 =    a   wholly contains but is not equal to       b
   *     0 =    a   is identical to                           b
   *    -1 =    b   wholly contains but is not equal to       a
   *    -2 =    b   covers later range of address space than  a
   */
  int al= a->prefixlen;
  int bl= b->prefixlen;
  int ml, d;

  if (al==bl) { ml=al; }
  else if (al<bl) { ml=al; if (ame_contains(a,b->prefix,bl)) return +1; }
  else if (bl<al) { ml=bl; if (ame_contains(b,a->prefix,al)) return -1; }

  d= memcmp(b->prefix, a->prefix, prefix_bytes(ml));
  return (d > 0 ? +2 :
	  d < 0 ? -2 :
	  0);
}

/*---------- searching maps ----------*/

typedef enum {
  sr_notfound,
  sr_exact,
  sr_inbig,
  sr_aroundsmall
} Search_Result;

static int
am_binarychop(AddrMap_Value *am, int low_oreq, int high_strict, void *u,
	      int (*test)(AddrMap_Entry *am, void *u) /* -ve => look left */,
	      int *found_r) {
  int mid, cmp;
  
  for (;;) {
    if (high_strict <= low_oreq) {
      assert(high_strict == low_oreq);
      *found_r= 0;
      return high_strict;
    }

    mid= (high_strict + low_oreq) / 2;
    cmp= test(&am->entries[mid], u);

    if (!cmp) {
      *found_r= 1;
      return mid;
    }

    if (cmp < 0)
      high_strict= mid;
    else
      low_oreq= mid+1;
  }
}

struct am_search_u {
  int forbid_aroundsmall;
  AddrMap_Entry proposed;
  Search_Result sr;
};

static int
am_search_binchoptest(AddrMap_Entry *ame, void *u_v) {
  struct am_search_u *u= u_v;
  int cmp;

  cmp= ame_compare(&u.proposed, ame);
  switch (cmp) {
  case -1:  u->sr= sr_inbig;        return  0;
  case  0:  u->sr= sr_exact;        return  0;
  case +1:  u->sr= sr_aroundsmall;  return  0;
  default:                          return cmp;
  }
}

static Search_Result
am_search(AddrMap_Value *am, const AddrMap_Entry *proposed, int *place_r) {
  int place, found;
  struct am_search_u u;

  u.forbid_aroundsmall= forbid_aroundsmall;
  u.proposed= proposed;
  u.sr= sr_notfound;

  *place_r= am_binarychop(am, 0, am.used, &u, am_search_binchoptest, &found);

  assert(!!found == (u.sr != sr_notfound));
  return u.sr;
}

/*---------- useful operations on AddrMap_Value etc. ----------*/

/*---------- amendment (complex algorithm) ----------*/

struct am_amend_aroundsmall_u {
  AddrMap_Entry *new;
  int sign;
};


static int
am_amend_aroundsmall_binchoptest(AddrMap_Entry *search, void *u_v) {
  struct am_amend_aroundsmall_u *u= u_v;

  cmp= u->sign * ame_compare(search, u->new);

  switch (cmp) {
  case +2:  return -u->sign;
  case +1:  return +u->sign;
  default:  abort();
  }
}

int do_addrmap_amend(ClientData cd, Tcl_Interp *ip,
		     AddrMap_Var map, Tcl_Obj *prefix,
		     Tcl_Obj *preflen, Tcl_Obj *data) {
  AddrMap_Value *am= map.am;
  AddrMap_Entry new, *fragment;
  AddrMap_Entry *breaking, *replacements;
  int rc, insertat, findend, cmp, nreplacements, new_used;
  struct am_amend_aroundsmall_u u;

  ame_init(&new);
  
  rc= ame_parsekey(ip,&new,prefix,preflen,0);  if (rc) return rc;

  sr= am_search(am, &new, &searched);

  replacements= &new;
  nreplacements= 1;
  replace_start= searched;
  replace_end= searched;

  switch (sr) {

  case sr_notfound:
    break;

  case sr_exact:
    replace_end= searched+1;
    break;

  case sr_aroundsmall:
    u.ame= new;
    u.sign= -1;
    replace_start= am_binarychop(am, 0, searched, &u,
				 am_amend_aroundsmall_binchoptest, &dummy);
    u.sign= +1;
    replace_end= am_binarychop(am, searched+1, am.used, &u,
				 am_amend_aroundsmall_binchoptest, &dummy);
    break;

  case sr_inbig:
    /* Urgh, we need to break it up.  This produces
     *   - innermost prefix (the new one) as specified
     *   - one for each bitlength
     *       <= innermost
     *        > outermost (the existing one)
     *     each one specifying the outermost prefix plus zero, one,
     *     two, etc. bits of the innermost followed by one bit
     *     opposite to the innermost, with the outermost's data
     * Eg, if we have ff/8=>A and want to amend so that ffff/16=>B
     * then we replace ff/8 with ff0/9=>A ff8/10=>A ffc/11=>A ...
     * ... fff8/14=>A fffc/15=>A fffe/16=>A ffff/16=>B.
     */

    breaking= &am.entries[searched];
    nreplacements= new.prefix - breaking->prefixlen + 1;
    fixme check integer overflow ^
    replacements= TALLOC(sizeof(*replacements) * nreplacements);

    for (fragmentlen= breaking->prefixlen + 1,
	   left_insert= 0, right_insert= nreplacements;
	 fragmentlen <= new.prefix;
	 fragmentlen++) {
      int fragmentbytes;

      fragmentbytes= prefix_bytes(fragmentlen)
      fragment->prefixlen= fragmentlen;
      fragment->prefix= TALLOC(fragmentbytes);
      memcpy(fragment->prefix, new.prefix, fragmentbytes);
      ame_clear_unwanted(fragment, fragmentbytes);

      fragment->prefix[fragmentbytes] ^=
	0x80u >> ((fragmentlen+7) & 7);

      switch (ame_compare(&fragment, &new)) {
      case -2:  replacements[left_insert++]=  fragment;  break;
      case +2:  replacements[--right_insert]= fragment;  break;
      default: abort();
      }
    }
    assert(left_insert == right_insert-1);
    replacements[left_insert]= new;
    ame_init(&new);

    replace_end= searched+1;
    break;

  }

  new_used= am.used - (replace_end - replace_start) + nreplacements;

  if (new_used > am.space)
    am_reallocentries(am, new_used * 2);

  for (scan=replacements, i=0;
       i < nreplacements;
       scan++, i++) {
    scan->data= data;
    Tcl_IncrRefCount(scan->data);
  }

  for (i= replace_start, scan= am.entries+i;
       i < replace_end;
       i++, scan++) {
    ame_free(scan);
  }

  memmove(am.entries + replace_start + nreplacements,
	  am.entries + replace_end,
	  sizeof(*am.entries) * (am.used - replace_end));

  memcpy(am.entries + replace_start,
	 replacements,
	 sizeof(*am.entries) * nreplacements);

  am.used= new_used;
  if (replacements != &new)
    /* we don't bother freeing the actual array elements because
     * if replacements!=&new the array is only full if we're
     * committed and have already copied the values into the actual
     * AddrMap_Value. */
    TFREE(replacements);

  return TCL_OK;
}

/*---------- other substantial operations on mask maps ----------*/

int do_addrmap_lookup(ClientData cd, Tcl_Interp *ip,
		      Tcl_Obj *mapo, HBytes_Value addrhb, Tcl_Obj *def,
		      Tcl_Obj **result) {
  AddrMap_Value *am= (void*)&mapo->internalRep;
  const Byte *addr= hbytes_data(&addrhb);
  int addrbytes= hbytes_len(&addrhb);
  int i, addrbits, place;
  Search_Result sr;

  addrbits= addrbytes * 8;
  sr= am_search(am, addr, addrbits, &place);

  switch (sr) {

  case sr_notfound:
    if (!def) return staticerr(ip, "address not found in addr-map",
			       "HBYTES ADDRMAP NOMATCH");
    *result= def;
    break;

  case sr_aroundsmall:
    return staticerr(ip, "address shorter than mask in map",
		     "HBYTES ADDRMAP UNDERRUN");

  case sr_exact:
  case sr_inbig:
    *result= am.entres[place].data;
    break;
    
  }

  return TCL_OK;
}

/*---------- Tcl type and arg parsing functions ----------*/

