import React, { Component } from "react";
import { Form, Button, Alert } from "react-bootstrap";

import "./paste.css";

import "highlight.js/styles/github.css";
import Highlight from "react-highlight";
import Markdown from 'react-markdown';

import pako from "pako";
import seedrandom from "seedrandom";
import { remoteService } from "./RemoteService"

import RIPEMD160 from "crypto-js/ripemd160";
import SHA256 from "crypto-js/sha256";
import AES from "crypto-js/aes";
import BASE64 from "crypto-js/enc-base64";
import HEX from "crypto-js/enc-hex";
import UTF8 from "crypto-js/enc-utf8"

const rng = seedrandom();
const req = remoteService();

const b64 = (s) => (!!s?BASE64.stringify(s).replace(/[=]+/, '').replace(/\//g, '_').replace(/\+/g, '-'):'');
const sha = (s) => b64(SHA256(s));
const chk = (s) => b64(RIPEMD160(SHA256(s)));
const enc = (t, p) => AES.encrypt(t, p).toString();
const u16a = (ua) => {
    let s = '';
    for (let i = 0; i < ua.length; i++) {
        s += ('0' + ua[i].toString(16)).slice(-2);
    }
    return HEX.parse(s);
};
const u8a = (wa) => {
    const w = wa.words;
    let b = new Uint8Array(w.length * 4), v, i, j, k = 0;
    for (i = 0; i < w.length; ++i) {
        v = w[i];
        for (j = 3; j >= 0; --j) {
            b[k++] = ((v >> 8 * j) & 0xFF);
        }
    }
    return b;
};
const str8 = (ua) => {
  let s = '';
  for (let i = 0; i < ua.byteLength; i++) {
      if ((ua[i]&0x80) === 0) s += String.fromCharCode(ua[i]);
      else if ((ua[i]&0xe0) === 0xc0 && (ua[i+1]&0xc0) === 0x80) {
        s += String.fromCharCode(((ua[i]&0x1f)<<6) + (ua[i+1]&0x3f));
        i += 1;
      }
      else if ((ua[i]&0xf0) === 0xe0 && (ua[i+1]&0xc0) === 0x80 && (ua[i+2]&0xc0) === 0x80){
        s += String.fromCharCode(((ua[i]&0x0f)<<12) + ((ua[i+1]&0x3f)<<6) + (ua[i+2]&0x3f));
        i += 2;
      }
      else if ((ua[i]&0xf8) === 0xf0 && (ua[i+1]&0xc0) === 0x80 && (ua[i+2]&0xc0) === 0x80 && (ua[i+3]&0xc0) === 0x80) {
        s += String.fromCharCode(((ua[i]&0x0f)<<18) + ((ua[i+1]&0x3f)<<12) + ((ua[i+2]&0x3f)<<6) + (ua[i+3]&0x3f));
        i += 3;
      }
      else { s += String.fromCharCode(65533); }
  }
  return s;
};
const zip = (text) => u16a(pako.gzip(text));
const dec = (c, p, z) => {
  if (z) {
      let tx = AES.decrypt(c, p);
      tx = u8a(tx);
      return str8(pako.inflate(tx));
  } else {
      return AES.decrypt(c, p).toString(UTF8);
  }
}
const blength = (o) => {
    if (!(typeof o === 'string' || o instanceof String)) return 0;
    if (o === undefined || o.length === undefined) return 0;
    var utf8length = 0;
    for (var n = 0; n < o.length; n++) {
        var c = o.charCodeAt(n);
        if (c < 128) {
            utf8length++;
        }
        else if ((c > 127) && (c < 2048)) {
            utf8length = utf8length + 2;
        } else {
            utf8length = utf8length + 3;
        }
    }
    return utf8length;
};

const PASTE_API = "https://paste.dn42.us/paste"

const syntaxItems = [
  ["text", "Plain Text"],
  ["apache", "Apache"],
  ["bash", "Bash"],
  ["coffeescript", "CoffeeScript"],
  ["cpp", "C++"],
  ["cs", "C#"],
  ["css", "CSS"],
  ["diff", "Diff"],
  ["http", "HTTP"],
  ["ini", "Ini"],
  ["java", "Java"],
  ["javascript", "JavaScript"],
  ["json", "JSON"],
  ["makefile", "Makefile"],
  ["markdown", "Markdown"],
  ["nginx", "Nginx"],
  ["objectivec", "Objective C"],
  ["perl", "Perl"],
  ["php", "PHP"],
  ["python", "Python"],
  ["ruby", "Ruby"],
  ["sql", "SQL"],
  ["xml", "HTML, XML"]
];
const expireItems = [
  [3600, "1 Hour"],
  [86400, "1 Day"],
  [604800, "1 Week"],
  [2419200, "4 Weeks"],
  [15778463, "6 Months"],
  [31556926, "1 Year"]
];


class Paste extends Component {
  constructor(props) {
    super(props);

    const { location } = props;

    const [ hash, key ] = location.hash.substring(2).split("!");

    this.state = {
      error:      "",
      syntax:     "text",
      expire:     604800,
      expires:    "",
      burn:       false,
      plain:      "",
      cipher:     "",
      decryptKey: hash !== "new" ? key  : "",
      hash:       hash !== "new" ? hash : "",
      entropy:    0,
      gzip:       false,
      syntaxItems,
      expireItems
    };

    this.startEntropy = this.startEntropy.bind(this);
    this.addEntropy = this.addEntropy.bind(this);
    this.onChange = this.onChange.bind(this);
    this.onSubmit = this.onSubmit.bind(this);
    this.onNew = this.onNew.bind(this);
    this.onCopy = this.onCopy.bind(this);
    this.encrypt = this.encrypt.bind(this);
    this.decrypt = this.decrypt.bind(this);
  }

  startEntropy(events, count) {
      let t = [];

      let fn = (e) => {
          t.push([e.pageX, e.pageY, e.keyCode, +new Date()]);
          if (t.length < count) {
              return;
          }
          this.addEntropy(t);
          t = [];
      };
      fn = fn.bind(this);

      for (let i in events) {
          if (events.hasOwnProperty(i))
                document.addEventListener(events[i], fn);
      }
  };

  addEntropy(s) {
    this.setState(function(state, props) {
      return {entropy: state.entropy + s.length};
    } );
    Math.seedrandom(s, {entropy: true});
  };

  onChange(event) {
    const target = event.target;
    const value = target.type === 'checkbox' ? target.checked : target.value;
    const name = target.name;

    this.setState({
      [name]: value
    });
  }

  onSubmit(event) {
    event.preventDefault();
    const { plain } = this.state;
    const { history } = this.props;

    this.encrypt(plain).then(([hash, decryptKey]) => { history.push('/paste/#/' + hash + '!' + decryptKey) });
  }
  onNew(event) {
    const { history } = this.props;
    this.setState({cipher:"", plain:"", hash: "", decryptKey: "", syntax: "text", expire: 604800, burn: false}, () => history.push('/paste'));
  }
  onCopy(event) {
    const { history } = this.props;
    this.setState({hash: "", decryptKey: ""}, () => history.push('/paste'))
  }

  decrypt(tx) {
    if (tx === "") {
      this.setState({hash: "", decryptKey: "", error: "Unable to retrieve paste."})
      return;
    }

    let s = tx.split('\n');
    let i = 0;

    let header = {};
    while (true) {
        if (s[i] === "") break;

        var l = s[i].trim().split(':\t');
        header[l[0]] = l[1];

        i++;
    }
    const cipher = s.splice(i).join('');
    const zip = !!header.zip && header.zip === "true";

    const { decryptKey } = this.state;
    const plain = dec(cipher, decryptKey, zip);
    const expires = !!header.exp ? (!!header.burn ? "Burn on Read" : ((d) => d.toLocaleDateString() + " " + d.toLocaleTimeString())(new Date(header.exp*1000))) : "Never";
    this.setState({cipher: tx, plain: plain, expires, syntax: header.lang});
  }

  encrypt(input) {
    const rnd = rng(40);
    const decryptKey = sha(rnd);

    const gzip = blength(input)>4000;
    const plain = gzip?zip(input):input;

    const { syntax, expire, burn } = this.state;

    const header = {
      chk: chk(rnd),
      lang: syntax,
      exp: parseInt(expire, 10) + (Date.now() / 1000 | 0),
      zip: gzip,
      burn: burn
    }

    let s = '', e = enc(plain, decryptKey);
    while (e.length > 79) {
      s += e.slice(0, 79) + "\n";
      e = e.slice(79);
    }
    s += e + "\n";

    const expires = !!header.exp ? (!!header.burn ? "Burn on Read" : ((d) => d.toLocaleDateString() + " " + d.toLocaleTimeString())(new Date(header.exp*1000))) : "Never";
    const cipher = Object.entries(header).map(([k, v]) => !!v ? k + ":\t" + v + "\n" : "").join('') + "\n" + s;

    return req(PASTE_API).post({}, cipher)
              .then((r) => r.text())
              .then((d) => {
                console.log("Received:\n" + d);
                const [ok='', hash=''] = d.split(' ', 2);
                if (ok === "OK")
                  this.setState((state) => ({cipher, gzip, decryptKey, hash, expires}));
                else console.log(d);
                return [hash, decryptKey];
            });
  }

  componentDidMount() {
    this.startEntropy(['mousemove', 'keydown', 'keypress', 'click', 'scroll'], 16);
    req(`${PASTE_API}/rng`).get().then((res)=>res.text()).then(this.addEntropy).catch();

    const { hash } = this.state;
    if (hash !== "")
      req(`${PASTE_API}/${hash}`).get().then((res)=> res.ok ? res.text() : "").then(this.decrypt).catch();
  }
  componentWillReceiveProps(nextProps) {
    const { location } = nextProps;
    const { hash:nextHash } = location;
    const [ hash='', key='' ] = nextHash.substring(2).split("!");

    if (hash === this.state.hash) return;

    if (hash === '') this.setState({cipher:"", plain:"", hash: "", decryptKey: "", syntax: "text", expire: 604800, burn: false})
    else {
      this.setState({hash: hash, decryptKey: key});
      req(`${PASTE_API}/${hash}`).get().then((res)=> res.ok ? res.text() : "").then(this.decrypt).catch();
    }
  }

  render() {
    const { hash } = this.state;
    return hash === '' ? <PasteCreate {...this.state} onChange={this.onChange} onSubmit={this.onSubmit}/> : <PasteView {...this.state} onNew={this.onNew} onCopy={this.onCopy}/> ;
  }
}


function PasteCreate({error, onSubmit, onChange, syntax, syntaxItems, expire, expireItems, burn, entropy, plain}) {
  return (
    <section className="container">
      <div>
        {!!error ?
          <Alert bsStyle="warning">
            <strong>Holy guacamole!</strong> {error}
          </Alert> : ""}

        <Form name='paste' onSubmit={onSubmit}>
          <div className="form form-inline">
            <ol className='breadcrumb'>
              <li>
                <label>Syntax</label>
                <select className='form-control input-sm' name="syntax" onChange={onChange} value={syntax}>
                  {syntaxItems.map((o) => <option key={o[0]} value={o[0]}>{o[1]}</option>)}
                </select>
              </li>
              <li>
                <label>Expires</label>
                <select className='form-control input-sm' name="expire" onChange={onChange} value={expire}>
                  {expireItems.map((o) => <option key={o[0]} value={o[0]}>{o[1]}</option>)}
                </select>
              </li>
              <li>
                <label><input type='checkbox' name="burn" onChange={onChange} value={burn}/> Burn on Read</label>
              </li>
            </ol>
          </div>

          <textarea required className='form-control' rows='20' name="plain" onChange={onChange} value={plain}></textarea>
          <pre>Additional Entropy: {entropy} bytes / Content size: {blength(plain)} bytes</pre>

          <Button type='submit' className='btn btn-default btn-lg btn-block'>Encrypt</Button>
        </Form>

        <p>Create pastes from the command line! <a href='./paste.sh' download>paste.sh</a></p>

        <pre>{`$ echo /etc/passwd | ./paste.sh

env options:
PASTE_URL  - Set the url base for paste operations (default: HTTPS://paste.dn42.us)
PASTE_GZIP - 0 = No Compression,  1 = Use gzip compression (default: 0)
PASTE_BURN - 0 = No Burn on Read, 1 = Burn on read         (default: 0)
PASTE_DATE - Value to be used when setting expire date.    (default: next-week)`}
        </pre>
      </div>
    </section>
  );
}

function PasteView({hash, decryptKey, expires, gzip, cipher, plain, syntax, onNew, onCopy}) {
  const gzipOpts = gzip ? '| gzip -dc' : '';
  return (
      <section className="container">
      <div className="input-group">
        <span className="input-group-btn">
          <a className="btn btn-default" type="button" onClick={onNew}>New</a>
        </span>
        <input type='text' readOnly className='form-control' value={`${window.location.origin}/paste/#/${hash}!${decryptKey}`} onClick={(e) => e.target.select()}/>
        <span className="input-group-btn">
          <a className="btn btn-default" type="button" onClick={onCopy}>Copy</a>
        </span>
      </div>

      <div ng-if="store.err == undefined">
        <div className='well well-sm'>
          <b>Lang:</b> {syntax}, <b>Expires:</b> {expires}
        </div>
        {syntax==="markdown" ? (
          <Markdown source={plain} />
        ) : (
          <Highlight className={syntax}>{plain}</Highlight>
        )}


        <pre>{`# Command Line:
curl -s "${PASTE_API}/${hash}" \\
  | sed "1,/^\\$/d" \\
  | openssl aes-256-cbc -md md5 \\
            -d -a -k "${decryptKey}" ${gzipOpts}

${cipher}`}</pre>
      </div>
    </section>
  );
}

export default Paste;
