/***************************************************************************
 *             __________               __   ___.
 *   Open      \______   \ ____   ____ |  | _\_ |__   _______  ___
 *   Source     |       _//  _ \_/ ___\|  |/ /| __ \ /  _ \  \/  /
 *   Jukebox    |    |   (  <_> )  \___|    < | \_\ (  <_> > <  <
 *   Firmware   |____|_  /\____/ \___  >__|_ \|___  /\____/__/\_ \
 *                     \/            \/     \/    \/            \/
 * $Id$
 *
 * Copyright (C) 2008 by William Poetra Yoga Hadisoeseno and Frank Gevaerts
 *
 * 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 software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
 * KIND, either express or implied.
 *
 ****************************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <libgen.h>
#include <arpa/inet.h>

#include <usb.h>

#define	bswap_16(value)  \
 	((((value) & 0xff) << 8) | ((value) >> 8))

#define	bswap_32(value)	\
 	(((uint32_t)bswap_16((uint16_t)((value) & 0xffff)) << 16) | \
 	(uint32_t)bswap_16((uint16_t)((value) >> 16)))

#define host_to_le32(_x) bswap_32(htonl(_x))
#define host_to_le16(_x) bswap_16(htons(_x))

void usage()
{
  fprintf(stderr, "usage: meizu_dfu m3 [SST39VF800.dfu] <M3.EBN>\n");
  fprintf(stderr, "       meizu_dfu m6 [SST39VF800.dfu] <M6.EBN>\n");
  fprintf(stderr, "       meizu_dfu m6sl [updateNAND_BE_070831.dfu] <M6SL.EBN>\n");
  exit(1);
}

uint32_t crc32(char *data, int len, uint32_t poly, uint32_t init)
{
  uint32_t crc_table[256];
  uint32_t crc, t;
  int i, j;

  // generate the table
  for (i = 0; i < 256; ++i) {
    t = i;
    for (j = 0; j < 8; ++j)
      if (t & 1)
        t = (t >> 1) ^ poly;
      else
        t >>= 1;
    crc_table[i] = t;
  }

  // calculate the crc
  crc = init;
  for (i = 0; i < len; ++i)
    crc = (crc >> 8) ^ crc_table[(crc^data[i]) & 0xff];

  return crc;
}

typedef struct {
  char *name;
  char *data;
  int len;
} image_data_t;

typedef struct {
  int delay;
  int pre_off;
  uint32_t pre_sig;
  uint16_t suf_dev;
  uint16_t suf_prod;
  uint16_t suf_ven;
  uint16_t suf_dfu;
  char suf_sig[3];
  uint8_t suf_len;
} image_attr_t;

#define BLOCK_SIZE 2048
#define DFU_TIMEOUT 0xa000
#define DFU_CRC_POLY 0xedb88320
#define DFU_INIT_CRC 0xffffffff

#define USB_VID_SAMSUNG 0x0419
#define USB_PID_M6SL    0x0145
#define USB_PID_M3_M6   0x0141

void init_img(image_data_t *img, const char *filename, image_attr_t *attr)
{
  int fd, len, i, readlen;
  struct stat statbuf;
  char buf[BLOCK_SIZE];
  uint32_t dfu_crc;
  uint32_t le_len;

  printf("Reading %s...", filename);

  if (stat(filename, &statbuf) < 0) {
    printf("\nCould not stat file, exiting.\n");
    exit(1);
  }
  len = statbuf.st_size;

  img->name = basename(strdup(filename));
  img->data = malloc(len + 16);
  img->len = len + 16;

  fd = open(filename, O_RDONLY);
  for (i = 0; i < len; i += BLOCK_SIZE) {
    readlen = ((len - i) < BLOCK_SIZE) ? (len - i) : BLOCK_SIZE;
    read(fd, buf, readlen);
    memcpy(img->data + i, buf, readlen);
  }
  close(fd);

  le_len = host_to_le32(img->len);
  // patch the data size in after the signature
  memcpy(img->data + attr->pre_off + 4, &le_len, 4);

  /* convert to little endian */
  attr->suf_dev = host_to_le16(attr->suf_dev);
  attr->suf_prod = host_to_le16(attr->suf_prod);
  attr->suf_ven = host_to_le16(attr->suf_ven);
  attr->suf_dfu = host_to_le16(attr->suf_dfu);

  // append the suffix (excluding the checksum)
  memcpy(img->data + len, &attr->suf_dev, 2);
  memcpy(img->data + len + 2, &attr->suf_prod, 2);
  memcpy(img->data + len + 4, &attr->suf_ven, 2);
  memcpy(img->data + len + 6, &attr->suf_dfu, 2);
  memcpy(img->data + len + 8, &attr->suf_sig, 3);
  memcpy(img->data + len + 11, &attr->suf_len, 1);

  dfu_crc = host_to_le32(crc32(img->data, len + 12, DFU_CRC_POLY, DFU_INIT_CRC));
  memcpy(img->data + len + 12, &dfu_crc, 4);

#if 0
  FILE *f = fopen(img->name, "w");
  fwrite(img->data, len + 16, 1, f);
  fclose(f);
#endif

  printf("OK\n");
}

usb_dev_handle *usb_dev_open(uint16_t dfu_vid, uint16_t dfu_pid)
{
  struct usb_bus *bus;
  struct usb_device *dev;
  usb_dev_handle *device;

  printf("USB initialization...");

  usb_init();
  usb_find_busses();
  usb_find_devices();

  for (bus = usb_get_busses(); bus != NULL; bus = bus->next)
    for (dev = bus->devices; dev != NULL; dev = dev->next)
      if (dev->descriptor.idVendor == dfu_vid
          && dev->descriptor.idProduct == dfu_pid)
        goto found;

  printf("\nNo device found, exiting.\n");
  exit(1);

found:
  printf(" Device found.\n");
  device = usb_open(dev);
  usb_claim_interface(device, 0);
  return device;
}

void usb_mimic_windows(usb_dev_handle *device)
{
  char data[1024];

  usb_control_msg(device, 0x80, 0x06, 0x0100, 0x0000, data, 0x0012, DFU_TIMEOUT);
  usb_control_msg(device, 0x80, 0x06, 0x0200, 0x0000, data, 0x0009, DFU_TIMEOUT);
  usb_control_msg(device, 0x80, 0x06, 0x0200, 0x0000, data, 0x001b, DFU_TIMEOUT);
  usb_control_msg(device, 0x80, 0x06, 0x0100, 0x0000, data, 0x0040, DFU_TIMEOUT);
  usb_control_msg(device, 0x80, 0x06, 0x0100, 0x0000, data, 0x0012, DFU_TIMEOUT);
  usb_control_msg(device, 0x80, 0x06, 0x0200, 0x0000, data, 0x0009, DFU_TIMEOUT);
  usb_control_msg(device, 0x80, 0x06, 0x0300, 0x0000, data, 0x00ff, DFU_TIMEOUT);
  usb_control_msg(device, 0x80, 0x06, 0x0303, 0x0409, data, 0x00ff, DFU_TIMEOUT);
  usb_control_msg(device, 0x80, 0x06, 0x0200, 0x0000, data, 0x00ff, DFU_TIMEOUT);
  usb_control_msg(device, 0x80, 0x06, 0x0300, 0x0000, data, 0x00ff, DFU_TIMEOUT);
  usb_control_msg(device, 0x80, 0x06, 0x0302, 0x0409, data, 0x00ff, DFU_TIMEOUT);
  usb_control_msg(device, 0x80, 0x06, 0x0300, 0x0000, data, 0x00ff, DFU_TIMEOUT);
  usb_control_msg(device, 0x80, 0x06, 0x0302, 0x0409, data, 0x00ff, DFU_TIMEOUT);
  usb_control_msg(device, 0x80, 0x06, 0x0100, 0x0000, data, 0x0012, DFU_TIMEOUT);
  usb_control_msg(device, 0x80, 0x06, 0x0200, 0x0000, data, 0x0209, DFU_TIMEOUT);
}

void usb_dev_close(usb_dev_handle *device)
{
  printf("Releasing interface...");

  usb_release_interface(device, 0);

  printf(" OK\n");
}

enum DFU_REQUEST {
  DFU_DETACH = 0,
  DFU_DOWNLOAD,
  DFU_UPLOAD,
  DFU_GETSTATUS,
  DFU_CLRSTATUS,
  DFU_GETSTATE,
  DFU_ABORT
};

void get_cpu(usb_dev_handle *device)
{
  char data[64];
  int req_out_if = USB_ENDPOINT_IN | USB_TYPE_CLASS | USB_RECIP_INTERFACE;
  int len;

  printf("GET CPU");

  // check for "S5L8700 Rev.1"
  len = usb_control_msg(device, req_out_if, 0xff, 0x0002, 0, data, 0x003f, DFU_TIMEOUT);
  if (len < 0) {
    printf("\nError trying to get CPU model, exiting.\n");
    exit(1);
  }

  memset(data + len, 0, 64 - len);
  printf(", got: %s\n", data);
}

void send_file(usb_dev_handle *device, image_data_t *img)
{
  char dfu_ret[6];
  char *data;
  int req_out_if = USB_ENDPOINT_OUT | USB_TYPE_CLASS | USB_RECIP_INTERFACE;
  int req_in_if = USB_ENDPOINT_IN | USB_TYPE_CLASS | USB_RECIP_INTERFACE;
  int len, idx, writelen, i;

  printf("Sending %s... ", img->name);

  len = img->len;
  data = img->data;

  // loop for the file
  for (i = 0, idx = 0; i < len; i += BLOCK_SIZE, ++idx) {
    writelen = ((len - i) < BLOCK_SIZE) ? (len - i) : BLOCK_SIZE;
    usb_control_msg(device, req_out_if, DFU_DOWNLOAD, idx, 0, data + i, writelen, DFU_TIMEOUT);
    dfu_ret[4] = 0x00;
    while (dfu_ret[4] != 0x05)
      usb_control_msg(device, req_in_if, DFU_GETSTATUS, 0, 0, dfu_ret, 6, DFU_TIMEOUT);
    printf("#");
  }

  usb_control_msg(device, req_out_if, DFU_DOWNLOAD, idx, 0, NULL, 0, DFU_TIMEOUT);
  dfu_ret[4] = 0x00;
  while (dfu_ret[4] != 0x07)
    usb_control_msg(device, req_in_if, DFU_GETSTATUS, 0, 0, dfu_ret, 6, DFU_TIMEOUT);

  printf(" OK\n");
}

void clear_status(usb_dev_handle *device)
{
  char dfu_ret[6];
  int usb_in_if = USB_ENDPOINT_IN | USB_TYPE_CLASS | USB_RECIP_INTERFACE;
  int usb_out_if = USB_ENDPOINT_OUT | USB_TYPE_CLASS | USB_RECIP_INTERFACE;

  printf("Clearing status...");

  dfu_ret[4] = 0x00;
  while (dfu_ret[4] != 0x08)
    usb_control_msg(device, usb_in_if, DFU_GETSTATUS, 0, 0, dfu_ret, 6, DFU_TIMEOUT);
  usb_control_msg(device, usb_out_if, DFU_CLRSTATUS, 0, 0, NULL, 0, DFU_TIMEOUT);

  printf(" OK\n");
}

void dfu_detach(usb_dev_handle *device)
{
  char usb_ret[4];
  int usb_in_oth = USB_ENDPOINT_IN | USB_TYPE_CLASS | USB_RECIP_OTHER;
  int usb_out_oth = USB_ENDPOINT_OUT | USB_TYPE_CLASS | USB_RECIP_OTHER;

  printf("Detaching...");

  usb_control_msg(device, usb_in_oth, DFU_DETACH, 0x0000, 3, usb_ret, 4, DFU_TIMEOUT);
  usb_control_msg(device, usb_out_oth, DFU_DOWNLOAD, 0x0010, 3, NULL, 0, DFU_TIMEOUT);
  usb_control_msg(device, usb_in_oth, DFU_DETACH, 0x0000, 3, usb_ret, 4, DFU_TIMEOUT);
  usb_control_msg(device, usb_in_oth, DFU_DETACH, 0x0000, 3, usb_ret, 4, DFU_TIMEOUT);
  usb_control_msg(device, usb_in_oth, DFU_DETACH, 0x0000, 3, usb_ret, 4, DFU_TIMEOUT);
  usb_control_msg(device, usb_in_oth, DFU_DETACH, 0x0000, 3, usb_ret, 4, DFU_TIMEOUT);
  usb_control_msg(device, usb_in_oth, DFU_DETACH, 0x0000, 3, usb_ret, 4, DFU_TIMEOUT);

  printf(" OK\n");
}

void dfu_m3_m6(char *file1, char *file2)
{
  image_data_t img1, img2;
  image_attr_t attr1, attr2;
  usb_dev_handle *device;

  attr1.delay = 1000;
  attr1.pre_off = 0x20;
  attr1.pre_sig = 0x44465543;
  attr1.suf_dev = 0x0100;
  attr1.suf_prod = 0x0140;
  attr1.suf_ven = 0x0419;
  attr1.suf_dfu = 0x0100;
  memcpy(attr1.suf_sig, "RON", 3);
  attr1.suf_len = 0x10;

  init_img(&img1, file1, &attr1);

  if (file2) {
    attr2.delay = 1000;
    attr2.pre_off = 0x20;
    attr2.pre_sig = 0x44465543;
    attr2.suf_dev = 0x0100;
    attr2.suf_prod = 0x0140;
    attr2.suf_ven = 0x0419;
    attr2.suf_dfu = 0x0100;
    memcpy(attr2.suf_sig, "UFD", 3);
    attr2.suf_len = 0x10;

    init_img(&img2, file2, &attr2);
  }

  device = usb_dev_open(USB_VID_SAMSUNG, USB_PID_M3_M6);
//  usb_mimic_windows();
  get_cpu(device);
  get_cpu(device);
  send_file(device, &img1);

  if (file2) {
    printf("Wait a sec (literally)...");
    sleep(1);
    printf(" OK\n");

    clear_status(device);
    get_cpu(device);
    send_file(device, &img2);
    dfu_detach(device);
  }

  usb_dev_close(device);
}

void dfu_m6sl(char *file1, char *file2)
{
  image_data_t img1, img2;
  image_attr_t attr1, attr2;
  usb_dev_handle *device;

  attr1.delay = 1000;
  attr1.pre_off = 0x20;
  attr1.pre_sig = 0x44465543;
  attr1.suf_dev = 0x0100;
  attr1.suf_prod = 0x0140;
  attr1.suf_ven = 0x0419;
  attr1.suf_dfu = 0x0100;
  memcpy(attr1.suf_sig, "UFD", 3);
  attr1.suf_len = 0x10;

  init_img(&img1, file1, &attr1);

  if (file2) {
    attr2.delay = 1000;
    attr2.pre_off = 0x20;
    attr2.pre_sig = 0x44465543;
    attr2.suf_dev = 0x0100;
    attr2.suf_prod = 0x0140;
    attr2.suf_ven = 0x0419;
    attr2.suf_dfu = 0x0100;
    memcpy(attr2.suf_sig, "UFD", 3);
    attr2.suf_len = 0x10;

    init_img(&img2, file2, &attr2);
  }

  device = usb_dev_open(USB_VID_SAMSUNG, USB_PID_M6SL);
  get_cpu(device);
  get_cpu(device);
  send_file(device, &img1);

  if (file2) {
    printf("Wait a sec (literally)...");
    sleep(1);
    printf(" OK\n");
    usb_dev_close(device);

    device = usb_dev_open(USB_VID_SAMSUNG, USB_PID_M6SL);
    get_cpu(device);
    get_cpu(device);
    send_file(device, &img2);
    dfu_detach(device);
  }

  usb_dev_close(device);
}


int main(int argc, char **argv)
{
  if (argc < 3 || argc > 4)
    usage();

  setvbuf(stdout, NULL, _IONBF, 0);

  char *second_file = (argc == 4) ? argv[3] : NULL;

  if (!strcmp(argv[1], "m3"))
    dfu_m3_m6(argv[2], second_file);
  else if (!strcmp(argv[1], "m6"))
    dfu_m3_m6(argv[2], second_file);
  else if (!strcmp(argv[1], "m6sl"))
    dfu_m6sl(argv[2], second_file);
  else
    usage();

  return 0;
}

