"""Original two-symbol adaptive phrase codec; integer codewords, no bit packing."""
from itertools import product
import json

def encode(text):
 if type(text) is not str or len(text)>16 or any(c not in 'AB' for c in text):raise ValueError('A/B text length0..16')
 table={'A':0,'B':1};phrase='';codes=[];trace=[]
 for symbol in text:
  joined=phrase+symbol
  if joined in table:phrase=joined
  else:
   code=table[phrase];codes.append(code);table[joined]=len(table);trace.append({'emitted_phrase':phrase,'code':code,'new_phrase':joined,'new_code':table[joined]});phrase=symbol
 if phrase:codes.append(table[phrase])
 return codes,trace,table

def decode(codes):
 if type(codes) is not list or len(codes)>16 or any(type(x) is not int or x<0 for x in codes):raise ValueError('0..16 nonnegative integer codewords')
 table=['A','B'];parts=[];trace=[];previous=None
 for code in codes:
  special=False
  if code<len(table):entry=table[code]
  elif code==len(table) and previous is not None:entry=previous+previous[0];special=True
  else:raise ValueError('unavailable first or future codeword')
  added=None
  if previous is not None:
   added={'phrase':previous+entry[0],'code':len(table)};table.append(added['phrase'])
  parts.append(entry)
  if sum(map(len,parts))>16:raise ValueError('decoded length budget16')
  trace.append({'input_code':code,'decoded_phrase':entry,'special_next_code':special,'added':added});previous=entry
 return ''.join(parts),trace,table

def demo():
 rows=[]
 for text in ['ABABABA','AAAA','ABBAAB','', 'A']:
  codes,et,ed=encode(text);back,dt,dd=decode(codes);assert back==text and ed=={s:i for i,s in enumerate(dd)};rows.append({'text':text,'codewords':codes,'encoder_additions':et,'decoder_steps':dt,'roundtrip':back,'same_final_dictionary':True})
 assert any(x['special_next_code'] for r in rows for x in r['decoder_steps'])
 checks=0
 for n in range(8):
  for chars in product('AB',repeat=n):
   text=''.join(chars);codes,_,ed=encode(text);back,_,dd=decode(codes);assert back==text and ed=={s:i for i,s in enumerate(dd)};checks+=1
 rejected=[]
 for label,fn in [('unknown_symbol',lambda:encode('AC')),('too_long_text',lambda:encode('A'*17)),('first_unavailable',lambda:decode([2])),('future_code',lambda:decode([0,4])),('bool_code',lambda:decode([True])),('negative_code',lambda:decode([-1])),('decoded_budget',lambda:decode([0,2,3,4,5,6]))]:
  try:fn()
  except ValueError:rejected.append(label)
  else:raise AssertionError('expected rejection')
 return {'initial_dictionary':{'A':0,'B':1},'authored_roundtrips':rows,'all_binary_messages_length0_to7':checks,'invalid_cases_rejected':rejected,'scope':'original bounded integer-codeword teaching codec with alphabet A/B; no EOF marker, file format, binary packing, dictionary reset, Java execution, security or compression-ratio claim'}
if __name__=='__main__':print(json.dumps(demo(),ensure_ascii=True,indent=2))
