text:
var go = {};
go;

var _ = require('lodash');
var moment = require('moment');
var vumigo = require('vumigo_v02');
var Q = require('q');
var Choice = vumigo.states.Choice;
var ChoiceState = vumigo.states.ChoiceState;
var HttpApi = vumigo.http.api.HttpApi;
var JsonApi = vumigo.http.api.JsonApi;

// override moment default century switch at '68 with '49
moment.parseTwoDigitYear = function (input) {
    return +input + (+input > 49 ? 1900 : 2000);
};

go.migration = {
  make_migration_state: function (app, next_state) {
    return (function(app) {
      return function (name) {
      // NOTE: only go through this if the migration flag is set
      if(!app.im.config.migration_flag) {
        return app.states.create(next_state);
      }

      return go.utils
        .is_migrated_user(
            app.im, go.utils.normalize_msisdn(app.contact.msisdn, '27'))
        .then(function (is_migrated) {
          if(is_migrated) {
            return app.states.create(next_state);
          } else {
            return new ChoiceState(name, {
              question: app.$(
                "MomConnect is doing maintenance so some features may be " +
                "unavailable. Reply STOP to opt-out via SMS. To change to" +
                " baby messaging try again next week."),
              choices: [
                new Choice('continue', app.$('Continue')),
              ],
              next: next_state
            });
          }
        });
      };
    })(app);
  }
};

go.utils = {
    // Shared utils lib

    // make choices options with options
    make_month_choices: function($, start, limit) {
            // start should be 0 for Jan - array position
            var choices = [
                    new Choice('01', $('Jan')),
                    new Choice('02', $('Feb')),
                    new Choice('03', $('Mar')),
                    new Choice('04', $('Apr')),
                    new Choice('05', $('May')),
                    new Choice('06', $('Jun')),
                    new Choice('07', $('Jul')),
                    new Choice('08', $('Aug')),
                    new Choice('09', $('Sep')),
                    new Choice('10', $('Oct')),
                    new Choice('11', $('Nov')),
                    new Choice('12', $('Dec'))
                ];

            var choices_show = [];
            var choices_show_count = 0;
            var end = start + limit;

            for (var i=start; i<end; i++) {
                var val = (i >= 12 ? (i-12) : i);
                choices_show[choices_show_count] = choices[val];
                choices_show_count++;
            }

            return choices_show;
    },

    get_today: function(config) {
        var today;
        if (config.testing_today) {
            today = new Date(config.testing_today);
        } else {
            today = new Date();
        }
        return today;
    },

    get_tomorrow: function(config) {
        var today = go.utils.get_today(config);
        var moment_tomorrow = moment(today).add(1, 'days');
        return moment_tomorrow.format('YYYY-MM-DD');
    },

    is_weekend: function(config) {
        var today = go.utils.get_today(config);
        var moment_today = moment.utc(today);
        return moment_today.format('dddd') === 'Saturday' ||
          moment_today.format('dddd') === 'Sunday';
    },

    is_public_holiday: function(config) {
        var today = go.utils.get_today(config);
        var moment_today = moment.utc(today);
        var date_as_string = moment_today.format('YYYY-MM-DD');
        return _.contains(config.public_holidays, date_as_string);
    },

    is_out_of_hours: function(config) {
        var today = go.utils.get_today(config);
        var moment_today = moment.utc(today);
        // get business hours from config, -2 for utc to local time conversion
        var opening_time = Math.min.apply(null, config.helpdesk_hours) - 2;
        var closing_time = Math.max.apply(null, config.helpdesk_hours) - 2;
        return (moment_today.hour() < opening_time || moment_today.hour() >= closing_time);
    },

    get_due_year_from_month: function(month, today) {
      // if due month is less than current month then mother must be due next year
      motoday = moment(today);
      if ((motoday.month()+1) > parseInt(month, 10)) {
        return motoday.year()+1;
      } else {
        return motoday.year();
      }
    },

    check_valid_number: function(input){
        // an attempt to solve the insanity of JavaScript numbers
        var numbers_only = new RegExp('^\\d+$');
        if (input !== '' && numbers_only.test(input) && !Number.isNaN(Number(input))){
            return true;
        } else {
            return false;
        }
    },

    check_valid_phone_number: function(input) {
        // check that it is a number, starts with 0, and has at 10 digits
        if (go.utils.check_valid_number(input) && input[0] === '0' && input.length === 10) {
            return true;
        } else {
            return false;
        }
    },

    check_number_in_range: function(input, start, end){
        return go.utils.check_valid_number(input) && (parseInt(input, 10) >= start) && (parseInt(input, 10) <= end);
    },

    double_digit_day: function(input) {
        input_numeric = parseInt(input, 10);
        if (parseInt(input, 10) < 10) {
            return "0" + input_numeric.toString();
        } else {
            return input_numeric.toString();
        }
    },

    validate_id_sa: function(id) {
        var i, c,
            even = '',
            sum = 0,
            check = id.slice(-1);

        if (id.length != 13 || id.match(/\D/)) {
            return false;
        }
        if (!moment(id.slice(0,6), 'YYMMDD', true).isValid()) {
            return false;
        }
        id = id.substr(0, id.length - 1);
        for (i = 0; id.charAt(i); i += 2) {
            c = id.charAt(i);
            sum += +c;
            even += id.charAt(i + 1);
        }
        even = '' + even * 2;
        for (i = 0; even.charAt(i); i++) {
            c = even.charAt(i);
            sum += +c;
        }
        sum = 10 - ('' + sum).charAt(1);
        return ('' + sum).slice(-1) == check;
    },

    is_valid_date: function(date, format) {
        // implements strict validation with 'true' below
        return moment(date, format, true).isValid();
    },

    get_entered_due_date: function(month, day, config) {
        var year = go.utils.get_due_year_from_month(month, go.utils.get_today(config));
        return (year +'-'+ month +'-'+ go.utils.double_digit_day(day));
    },

    get_entered_birth_date: function(year, month, day) {
      return year +'-'+ month +'-'+ go.utils.double_digit_day(day);
    },

    extract_id_dob: function(id) {
        return moment(id.slice(0,6), 'YYMMDD').format('YYYY-MM-DD');
    },

    is_true: function(bool) {
        //If is is not undefined and boolean is true
        return (!_.isUndefined(bool) && (bool==='true' || bool===true));
    },

    readable_sa_msisdn: function(msisdn) {
        readable_no = '0' + msisdn.slice(msisdn.length-9, msisdn.length);
        return readable_no;
    },

    normalize_msisdn: function(raw, country_code) {
        // don't touch shortcodes
        if (raw.length <= 5) {
            return raw;
        }
        // remove chars that are not numbers or +
        raw = raw.replace(/[^0-9+]/g);
        if (raw.substr(0,2) === '00') {
            return '+' + raw.substr(2);
        }
        if (raw.substr(0,1) === '0') {
            return '+' + country_code + raw.substr(1);
        }
        if (raw.substr(0,1) === '+') {
            return raw;
        }
        if (raw.substr(0, country_code.length) === country_code) {
            return '+' + raw;
        }
        return raw;
    },

    incr_user_extra: function(data_to_increment, amount_to_increment) {
        if (_.isUndefined(data_to_increment)) {
            new_data_amount = 1;
        } else {
            new_data_amount = parseInt(data_to_increment, 10) + amount_to_increment;
        }
        return new_data_amount.toString();
    },

    get_timestamp: function() {
        return moment().format("YYYYMMDDHHmmss");
    },

    get_subscription_type: function(type){
      var types = {
        "optout": 4,
        "babyloss": 5,
        "servicerating": 6,
        "nurse_optout": 8
      };
      return types[type];
    },

    get_swt: function(im) {
        if (im.config.name.substring(0,9) === "nurse_sms") {
            return 4;  // swt = 4 for nurse sms optout
        } else if (im.config.name.substring(0,10) === "nurse_ussd") {
            return 3;  // swt = 3 for nurse ussd optout
        } else if (im.config.name.substring(0,10) === "smsinbound") {
            return 2;  // swt = 2 for sms optout
        } else {
            return 1;  // swt = 1 for ussd optout
        }
    },

    get_faccode: function(contact) {
        return contact.extra.clinic_code || null;
    },

    incr_kv: function(im, name) {
        return im.api_request('kv.incr', {key: name, amount: 1})
            .then(function(result){
                return result.value;
            });
    },

    decr_kv: function(im, name) {
        return im.api_request('kv.incr', {key: name, amount: -1})
            .then(function(result){
                return result.value;
            });
    },

    set_kv: function(im, name, value) {
        return im.api_request('kv.set',  {key: name, value: value})
            .then(function(result){
                return result.value;
            });
    },

    get_kv: function(im, name, default_value) {
        // returns the default if null/undefined
        return im.api_request('kv.get',  {key: name})
            .then(function(result){
                if(result.value === null) return default_value;
                return result.value;
            });
    },

    adjust_percentage_registrations: function(im, metric_prefix) {
        return Q.all([
            go.utils.get_kv(im, [metric_prefix, 'no_incomplete_registrations'].join('.'), 0),
            go.utils.get_kv(im, [metric_prefix, 'no_complete_registrations'].join('.'), 0)
        ]).spread(function(no_incomplete, no_complete) {
            var total_attempted = no_incomplete + no_complete;
            var percentage_incomplete = parseFloat(((no_incomplete / total_attempted) * 100).toFixed(2));
            var percentage_complete = parseFloat(((no_complete / total_attempted) * 100).toFixed(2));
            return Q.all([
                im.metrics.fire.last([metric_prefix, 'percent_incomplete_registrations'].join('.'), percentage_incomplete),
                im.metrics.fire.last([metric_prefix, 'percent_complete_registrations'].join('.'), percentage_complete)
            ]);
        });
    },

    incr_kv_conversions: function(im, contact, env) {
        var is_reg_by = contact.extra.is_registered_by;
        if (is_reg_by === 'personal' || is_reg_by === 'chw') {
            return go.utils.incr_kv(im, [env, is_reg_by, 'conversions_to_clinic'].join('.'));
        }
    },

    adjust_conversion_rates: function(im, env) {
        return Q.all([
            go.utils.get_kv(im, [env, 'personal', 'conversion_registrations'].join('.'), 0),
            go.utils.get_kv(im, [env, 'chw', 'conversion_registrations'].join('.'), 0),
            go.utils.get_kv(im, [env, 'personal', 'conversions_to_clinic'].join('.'), 0),
            go.utils.get_kv(im, [env, 'chw', 'conversions_to_clinic'].join('.'), 0)
        ]).spread(function(personal_regs, chw_regs, personal_convs, chw_convs) {
            if (personal_regs > 0 && chw_regs > 0) {
                var personal_conv_rate = parseFloat(((personal_convs / personal_regs) * 100).toFixed(2));
                var chw_conv_rate = parseFloat(((chw_convs / chw_regs) * 100).toFixed(2));
                return Q.all([
                    im.metrics.fire.last([env, 'personal', 'conversion_rate'].join('.'), personal_conv_rate),
                    im.metrics.fire.last([env, 'chw', 'conversion_rate'].join('.'), chw_conv_rate)
                ]);
            } else {
                return Q();
            }
        });
    },

    adjust_percentage_serviceratings: function(im, metric_prefix) {
        return Q.all([
            go.utils.get_kv(im, [im.config.metric_store, metric_prefix, 'sum', 'servicerating_start'].join('.'), 0),
            go.utils.get_kv(im, [im.config.metric_store, metric_prefix, 'sum', 'servicerating_success'].join('.'), 0)
        ]).spread(function(no_started, no_finished) {
            var percentage_complete = parseFloat(((no_finished / no_started) * 100).toFixed(2));
            var percentage_incomplete = 100 - percentage_complete;
            return Q.all([
                im.metrics.fire.last([metric_prefix, 'percent_incomplete_serviceratings'].join('.'), percentage_incomplete),
                im.metrics.fire.last([metric_prefix, 'percent_complete_serviceratings'].join('.'), percentage_complete)
            ]);
        });
    },

    fire_users_metrics: function(im, store_name, env, metric_prefix) {
        return go.utils.incr_kv(im, [store_name, 'unique_users'].join('.'))
            .then(function() {
                return Q.all([
                    go.utils.get_kv(im, [env, 'clinic', 'unique_users'].join('.'), 0),
                    go.utils.get_kv(im, [env, 'chw', 'unique_users'].join('.'), 0),
                    go.utils.get_kv(im, [env, 'personal', 'unique_users'].join('.'), 0)
                ]).spread(function(clinic_users, chw_users, personal_users) {
                    var total_users = clinic_users + chw_users + personal_users;
                    var clinic_percentage = (clinic_users / total_users) * 100;
                    var chw_percentage = (chw_users / total_users) * 100;
                    var personal_percentage = (personal_users / total_users) * 100;
                    return Q.all([
                        im.metrics.fire.inc([metric_prefix, 'sum', 'unique_users'].join('.')),
                        im.metrics.fire.last([env, 'clinic', 'percentage_users'].join('.'), clinic_percentage),
                        im.metrics.fire.last([env, 'chw', 'percentage_users'].join('.'), chw_percentage),
                        im.metrics.fire.last([env, 'personal', 'percentage_users'].join('.'), personal_percentage),
                        im.metrics.fire.inc([env, 'sum', 'unique_users'].join('.'))
                    ]);
                });
            });
    },

    jembi_clinic_validate: function (im, clinic_code) {
        var params = {
            'criteria': 'code:' + clinic_code
        };
        return go.utils
            .jembi_json_api_call('get', params, null, 'facilityCheck', im);
    },

    validate_clinic_code: function(im, clinic_code) {
        if (!go.utils.check_valid_number(clinic_code) ||
            clinic_code.length !== 6) {
            return Q()
                .then(function() {
                    return false;
                });
        } else {
            return go.utils
                .jembi_clinic_validate(im, clinic_code)
                .then(function(json_result) {
                    var rows = JSON.parse(json_result.data).rows;
                    if (rows.length === 0) {
                        return false;
                    } else {
                        return rows[0][2];
                    }
                });
        }
    },

    jembi_nc_clinic_validate: function (im, clinic_code) {
        var params = {
            'criteria': 'value:' + clinic_code
        };
        return go.utils
            .jembi_json_api_call('get', params, null, 'NCfacilityCheck', im);
    },

    validate_nc_clinic_code: function(im, clinic_code) {
        if (!go.utils.check_valid_number(clinic_code) ||
            clinic_code.length !== 6) {
            return Q()
                .then(function() {
                    return false;
                });
        } else {
            return go.utils
                .jembi_nc_clinic_validate(im, clinic_code)
                .then(function(json_result) {
                    var rows = JSON.parse(json_result.data).rows;
                    if (rows.length === 0) {
                        return false;
                    } else {
                        return rows[0][2];
                    }
                });
        }
    },

    is_alpha_numeric_only: function(input) {
        alpha_numeric = new RegExp('^[A-Za-z0-9]+$');
        return alpha_numeric.test(input);
    },

    get_servicerating_data: function(im) {
        var servicerating_data = [];
        for (var question in im.user.answers) {
            servicerating_data.push({
                "question": question,
                "answer": im.user.answers[question]
            });
        }
        return servicerating_data;
    },

    build_servicerating_json: function(im, contact, type) {
        var JSON_template = {
          "mha": 1,
          "swt": go.utils.get_swt(im),
          // "supplier_unique_id": servicerating_id,  // Marked as Optional in mini-scope and custom
                                                      // api doesn't provide an id so not submitting
          "dmsisdn": contact.msisdn,
          "cmsisdn": contact.msisdn,
          "type": go.utils.get_subscription_type(type),
          "faccode": go.utils.get_faccode(contact),
          "encdate": go.utils.get_timestamp(),
          "data": go.utils.get_servicerating_data(im)
        };
        return JSON_template;
    },

    jembi_send_servicerating: function(im, contact, metric_prefix, type) {
        var built_json = go.utils.build_servicerating_json(im, contact, type);
        return go.utils
            .jembi_json_api_call('post', null, built_json, 'serviceRating', im)
            .then(function(json_result) {
                var metric_name = [metric_prefix, "sum", "servicerating_to_jembi"].join('.');
                return go.utils.json_success_fail_metric(im, metric_name, json_result);
            });
    },

    get_patient_id: function(contact) {
        var formatter = {
            'sa_id': function () {
                return contact.extra.sa_id + '^^^ZAF^NI';
            },
            'passport': function () {
                return contact.extra.passport_no + '^^^' + contact.extra.passport_origin.toUpperCase() + '^PPN';
            },
            'none': function () {
                return contact.msisdn.replace('+', '') + '^^^ZAF^TEL';
            }
        }[contact.extra.id_type];
        return formatter();
    },

    get_nurse_id: function(contact) {
        var formatter = {
            'sa_id': function () {
                return contact.extra.nc_sa_id_no + '^^^ZAF^NI';
            },
            'passport': function () {
                return contact.extra.nc_passport_num + '^^^' + contact.extra.nc_passport_country.toUpperCase() + '^PPN';
            }
        }[contact.extra.nc_id_type];

        if (_.isUndefined(contact.extra.nc_id_type) || contact.extra.nc_id_type === null) {
            return contact.msisdn.replace('+', '') + '^^^ZAF^TEL';
        } else {
            return formatter();
        }
    },

    get_dob: function(contact) {
        if (!_.isUndefined(contact.extra.dob)) {
            return moment(contact.extra.dob, 'YYYY-MM-DD').format('YYYYMMDD');
        } else if (!_.isUndefined(contact.extra.nc_dob)) {
            return moment(contact.extra.nc_dob, 'YYYY-MM-DD').format('YYYYMMDD');
        } else {
            return null;
        }
    },

    get_optoutreason: function(contact) {
        var optoutreason_map = {
            "miscarriage": 1,
            "stillbirth": 2,
            "babyloss": 3,
            "not_useful": 4,
            "other": 5,
            "unknown": 6,
            "job_change": 7,
            "number_owner_change": 8
        };
        return optoutreason_map[contact.extra.opt_out_reason] || 6;
    },

    build_jembi_json: function(im, contact, user, type) {
        var JSON_template = {
            "mha": 1,
            "swt": go.utils.get_swt(im),
            "dmsisdn": user.msisdn,
            "cmsisdn": contact.msisdn,
            "id": go.utils.get_patient_id(contact),
            "type": go.utils.get_subscription_type(type),
            "lang": contact.extra.language_choice,
            "encdate": go.utils.get_timestamp(),
            "faccode": go.utils.get_faccode(contact),
            "dob": go.utils.get_dob(contact)
        };
        if (type === 'optout') {
            JSON_template.optoutreason = go.utils.get_optoutreason(contact);
        }
        return JSON_template;
    },

    get_nurse_optoutreason: function(contact) {
        var optoutreason_map = {
            "miscarriage": 1,
            "stillbirth": 2,
            "babyloss": 3,
            "not_useful": 4,
            "other": 5,
            "unknown": 6,
            "job_change": 7,
            "number_owner_change": 8
        };
        return optoutreason_map[contact.extra.nc_opt_out_reason] || 6;
    },

    jembi_optout_send_json: function(contact, user, type, im, metric_prefix) {
        var built_json = go.utils.build_jembi_json(im, contact, user, type);
        return go.utils
            .jembi_json_api_call('post', null, built_json, 'optout', im)
            .then(function(json_result) {
                var metric_name = [metric_prefix, "sum", "optout_to_jembi"].join('.');
                return go.utils.json_success_fail_metric(im, metric_name, json_result);
        });
    },

    build_nurse_jembi_json: function(im, contact, user, type) {
        var JSON_template = {
            "mha": 1,
            "swt": go.utils.get_swt(im),
            "dmsisdn": user.msisdn,
            "cmsisdn": contact.msisdn,
            "type": go.utils.get_subscription_type(type),
            "encdate": go.utils.get_timestamp(),
            "faccode": contact.extra.nc_faccode || null,
            "dob": go.utils.get_dob(contact) || null,
            "id": go.utils.get_nurse_id(contact),
            "optoutreason": go.utils.get_nurse_optoutreason(contact)
        };
        return JSON_template;
    },

    jembi_nurse_optout_send_json: function(contact, user, type, im, metric_prefix) {
        var built_json = go.utils.build_nurse_jembi_json(im, contact, user, type);
        return go.utils
            .jembi_json_api_call('post', null, built_json, 'nc/optout', im);
    },

    jembi_babyloss_send_json: function(contact, user, type, im, metric_prefix) {
        var built_json = go.utils.build_jembi_json(im, contact, user, type);
        return go.utils
            .jembi_json_api_call('post', null, built_json, 'subscription', im)
            .then(function(json_result) {
                var metric_name = [metric_prefix, "sum", "babyloss_to_jembi"].join('.');
                return go.utils.json_success_fail_metric(im, metric_name, json_result);
        });
    },

    json_success_fail_metric: function(im, metric_name, json_result) {
        var metric_to_fire = json_result.code >= 200 && json_result.code < 300
            ? metric_name + '_success'
            : metric_name + '_fail';
        return im.metrics.fire.inc(metric_to_fire, {amount: 1});
    },

    jembi_json_api_call: function(method, params, payload, endpoint, im) {
        var http = new HttpApi(im, {
            auth: {
                username: im.config.jembi.username,
                password: im.config.jembi.password
            },
            headers: {
                'Content-Type': ['application/json']
            }
        });
        switch(method) {
            case "post":
                return http.post(im.config.jembi.url_json + endpoint, {
                    data: JSON.stringify(payload)
                });
            case "get":
                return http.get(im.config.jembi.url_json + endpoint, {
                    params: params
                });
        }
    },

    control_api_call: function (method, params, payload, endpoint, im) {
        var http = new HttpApi(im, {
          headers: {
            'Content-Type': ['application/json'],
            'Authorization': ['ApiKey ' + im.config.control.username + ':' + im.config.control.api_key]
          }
        });
        switch (method) {
          case "post":
            return http.post(im.config.control.url + endpoint, {
                data: JSON.stringify(payload)
              });
          case "get":
            return http.get(im.config.control.url + endpoint, {
                params: params
              });
          case "patch":
            return http.patch(im.config.control.url + endpoint, {
                data: JSON.stringify(payload)
              });
          case "put":
            return http.put(im.config.control.url + endpoint, {
                params: params,
                data: JSON.stringify(payload)
              });
          case "delete":
            return http.delete(im.config.control.url + endpoint);
        }
    },

    control_v2_api_call: function (method, params, payload, endpoint, im) {
        var http = new JsonApi(im, {
            headers: {
                'Authorization': ['Token ' + im.config.control_v2.api_token]
            }
        });
        switch (method) {
            case "post":
                return http.post(im.config.control_v2.url + endpoint, {
                    data: payload
                });
            case "get":
                return http.get(im.config.control_v2.url + endpoint, {
                    params: params
                });
            case "patch":
                return http.patch(im.config.control_v2.url + endpoint, {
                    data: payload
                });
            case "put":
                return http.put(im.config.control_v2.url + endpoint, {
                    params: params,
                  data: payload
                });
            case "delete":
                return http.delete(im.config.control_v2.url + endpoint);
            }
    },

    is_migrated_user: function (im, msisdn) {
      var http = new JsonApi(im, {
          headers: {
              'Authorization': ['Token ' + im.config.identity_store.api_token]
          }
      });
      return http
        .get(im.config.identity_store.url + '/identities/search/', {
          params: {
            'details__addresses__msisdn': msisdn,
          }
        })
        .then(function (response) {
          return response.data.count > 0;
        });
    },

    subscription_type_and_rate: function(contact, im) {
        // Returns the subscription type, rate and start point
        // for loss and baby message subscriptions
        var response = {
            sub_type: null,
            sub_rate: null,
            sub_seq_start: 1
        };
        if (im.config.name.substring(0,6) == "optout") {
            response.sub_type = im.config.subscription[im.user.answers.states_start];
            response.sub_rate = im.config.rate.two_per_week;
        } else if (im.config.name.substring(0,10) == "smsinbound") {
            response.sub_type = im.config.subscription.baby1;
            response.sub_rate = im.config.rate.two_per_week;
        }
        return response;
    },

    get_edd: function(im, contact) {
        // Return estimated due date YYYY-MM-DD, default to null if unknown
        if (!_.isUndefined(contact.extra.due_date_month) &&
            !_.isUndefined(contact.extra.due_date_day)) {
            var day = contact.extra.due_date_day;
            var month = contact.extra.due_date_month;
            var year = go.utils.get_due_year_from_month(month, go.utils.get_today(im.config));
            return [year, month, day].join('-');
        } else {
            return null;
        }
    },

    get_identification_no: function(contact) {
        if (contact.extra.id_type === 'sa_id') {
            return contact.extra.sa_id;
        } else if (contact.extra.id_type === 'passport') {
            return contact.extra.passport_no;
        } else {
            return null;
        }
    },

    get_hcw_no: function(device_msisdn, contact_msisdn) {
        var n_device_msisdn = go.utils.normalize_msisdn(device_msisdn, '27');
        var n_contact_msisdn = go.utils.normalize_msisdn(contact_msisdn, '27');
        if (n_device_msisdn === n_contact_msisdn) {
            return null;
        } else {
            return n_device_msisdn;
        }
    },

    get_passport_origin: function(id_type, passport_origin) {
        if (id_type === 'passport') {
            return passport_origin;
        } else {
            return null;
        }
    },

    post_registration: function(device_msisdn, contact, im, reg_type) {
        var payload = {
            hcw_msisdn: go.utils.get_hcw_no(device_msisdn, contact.msisdn),  // +27...
            mom_msisdn: go.utils.normalize_msisdn(contact.msisdn, '27'),  // +27...
            mom_id_type: contact.extra.id_type,  // 'sa_id' | 'passport' | 'none'
            mom_passport_origin: go.utils.get_passport_origin(
                contact.extra.id_type, contact.extra.passport_origin),
            mom_lang: contact.extra.language_choice,  // 'en' | 'af' | 'xh' ...
            mom_edd: go.utils.get_edd(im, contact),  // 'YYYY-MM-DD' | null
            mom_id_no: go.utils.get_identification_no(contact),
            mom_dob: contact.extra.dob || null,  // 'YYYY-MM-DD' | null
            consent: contact.extra.consent === 'true' || null, // 'true' | null
            clinic_code: contact.extra.clinic_code || null,
            authority: reg_type,  // 'clinic' | 'chw' | 'personal'
        };
        return go.utils
            .control_v2_api_call("post", null, payload, 'registrations/', im)
            .then(function(post_response) {
                return go.utils.json_success_fail_metric(im, 'registration_call', post_response);
            });
    },

    post_nursereg: function(im, contact, dmsisdn, rmsisdn) {
        var payload = {
            cmsisdn: go.utils.normalize_msisdn(contact.msisdn, '27'),  // +27...
            dmsisdn: go.utils.normalize_msisdn(dmsisdn, '27'),  // +27...
            faccode: contact.extra.nc_faccode,
            id_type: contact.extra.nc_id_type,
            dob: contact.extra.nc_dob,
            sanc_reg_no: contact.extra.nc_sanc || null,
            persal_no: contact.extra.nc_persal || null
        };

        if (contact.extra.nc_id_type === 'sa_id') {
            payload.id_type = 'sa_id';
            payload.id_no = contact.extra.nc_sa_id_no;
            payload.dob = contact.extra.nc_dob;
        } else if (contact.extra.nc_id_type === 'passport') {
            payload.id_type = 'passport';
            payload.id_no = contact.extra.nc_passport_num;
            payload.passport_origin = contact.extra.nc_passport_country;
            payload.dob = contact.extra.nc_dob;
        } else {
            payload.id_type = null;
            payload.id_no = null;
            payload.dob = null;
        }

        if (rmsisdn) {
            payload.rmsisdn = rmsisdn;
        }

        return go.utils
            .control_v2_api_call("post", null, payload, 'nurseregs/', im);
    },

    post_subscription: function(contact, im, metric_prefix, env, opts) {
        var payload = {
            contact_key: contact.key,
            lang: contact.extra.language_choice,
            message_set: "/api/v1/message_set/" + opts.sub_type + "/",
            next_sequence_number: opts.sub_seq_start,
            schedule: "/api/v1/periodic_task/" + opts.sub_rate + "/",
            to_addr: contact.msisdn,
            user_account: contact.user_account
        };
        return go.utils
            .control_api_call("post", null, payload, 'subscription/', im)
            .then(function(doc_result) {
                if (doc_result.code >= 200 && doc_result.code < 300){
                    return Q.all([
                        im.metrics.fire.inc([metric_prefix, "sum", "subscription_to_protocol_success"].join('.'), {amount:1}),
                        im.metrics.fire.inc([env, "sum", "subscriptions"].join('.'), {amount:1})
                    ]);
                } else {
                    //TODO - implement proper fail issue #36
                    return im.metrics.fire.inc([metric_prefix, "sum", "subscription_to_protocol_fail"].join('.'), {amount:1});
                }
        });
    },

    get_subscriptions_by_msisdn: function(msisdn, im) {
        var params = {
            to_addr: msisdn
        };
        return go.utils
            .control_api_call("get", params, null, 'subscription/', im)
            .then(function(result) {
                return JSON.parse(result.data);
            });
    },

    subscription_unsubscribe_all: function(contact, im) {
        return go.utils
            .get_subscriptions_by_msisdn(contact.msisdn, im)
            .then(function(update) {
                var clean = true;  // clean tracks if api call is unnecessary
                for (i=0;i<update.objects.length;i++) {
                    if (update.objects[i].active === true){
                        update.objects[i].active = false;
                        clean = false;
                    }
                }
                if (!clean) {
                    return go.utils.control_api_call("patch", {}, update, 'subscription/', im);
                } else {
                    return Q();
                }
        });
    },

    get_nursereg_by_id: function(nursereg_id, im) {
        return go.utils
            .control_v2_api_call("get", null, null,
                'nurseregistrations/' + nursereg_id + '/', im)
            .then(function(get_response) {
                return get_response.data;
            });
    },

    patch_last_reg: function(contact, im, optout_reason) {
        return go.utils
            .get_nursereg_by_id(contact.extra.nc_last_reg_id, im)
            .then(function(nursereg) {
                nursereg.opted_out = true;
                nursereg.optout_count += 1;
                nursereg.optout_reason = optout_reason;
                return go.utils.control_v2_api_call("patch", null, nursereg,
                    'nurseregistrations/' + contact.extra.nc_last_reg_id + '/',
                    im);
            });
    },

    subscription_count_active: function(contact, im) {
        // This function is used in the public line to determine routing
        // depending on whether users have an active subscription
        var params = {
            to_addr: contact.msisdn
        };
        return go.utils
            .control_api_call("get", params, null, 'subscription/', im)
            .then(function(json_result) {
                var subs = JSON.parse(json_result.data);
                var active = 0;
                for (i=0;i<subs.objects.length;i++) {
                    if (subs.objects[i].active === true) {
                        active++;
                    }
                }
                return active;
            });
    },

    support_log_ticket: function(message, contact, im, metric_prefix) {
        var payload = {
          conversation: "/api/v1/snappybouncer/conversation/key/" + im.config.snappybouncer.conversation + "/",
          message: message,
          contact_key: contact.key,
          msisdn: contact.msisdn,
          faccode: parseInt(contact.extra.clinic_code, 10) || null
        };
        return go.utils
            .control_api_call("post", null, payload, 'snappybouncer/ticket/', im)
            .then(function(doc_result) {
                var metric;
                if (doc_result.code >= 200 && doc_result.code < 300){
                    metric = (([metric_prefix, "sum", "ticket_logged_to_control_success"].join('.')));
                } else {
                    //TODO - implement proper fail issue #36
                    metric = (([metric_prefix, "sum", "ticket_logged_to_control_fail"].join('.')));
                }
                return im.metrics.fire.inc(metric, {amount: 1});
        });
    },

    servicerating_log: function(contact, im, metric_prefix) {
        var payload = {
            "user_account": contact.user_account,
            "conversation_key": im.config.conversation_key,
            "contact": contact,
            "answers": im.user.answers
        };
        return go.utils
            .control_api_call("post", null, payload, 'servicerating/rate/', im)
            .then(function(doc_result) {
                var metric;
                if (doc_result.code >= 200 && doc_result.code < 300){
                    metric = (([metric_prefix, "sum", "servicerating_success"].join('.')));
                } else {
                    //TODO - implement proper fail issue #36
                    metric = (([metric_prefix, "sum", "subscription_to_protocol_fail"].join('.')));
                }
                return im.metrics.fire.inc(metric, {amount: 1});
        });
    },

    get_snappy_topics: function (im, faq_id) {
        var http = new JsonApi(im, {
          auth: {
            username: im.config.snappy.username,
            password: 'x'
          }
        });
        return http.get(im.config.snappy.endpoint + 'account/'+im.config.snappy.account_id+'/faqs/'+faq_id+'/topics', {
          data: JSON.stringify(),
          headers: {
            'Content-Type': ['application/json']
          }
        });
    },

    get_snappy_topic_content: function(im, faq_id, topic_id) {
        var http = new JsonApi(im, {
          auth: {
            username: im.config.snappy.username,
            password: 'x'
          }
        });
        return http.get(im.config.snappy.endpoint + 'account/'+im.config.snappy.account_id+'/faqs/'+faq_id+'/topics/'+topic_id+'/questions', {
          data: JSON.stringify(),
          headers: {
            'Content-Type': ['application/json']
          }
        });
    },

    set_language: function(user, contact) {
        if (contact.extra.language_choice !== null) {
            return user.set_lang(contact.extra.language_choice);
        } else {
            return Q();
        }
    },

    timed_out: function(im) {
        return im.msg.session_event === 'new'
            && im.user.state.name
            && im.user.state.name !== 'states_start';
    },

    get_reg_source: function(contact) {
        var reg_source;
        var reg_options = ['clinic', 'chw', 'personal'];
        if (!_.contains(reg_options, contact.extra.is_registered_by)) {
            reg_source = 'unknown';
        } else {
            reg_source = contact.extra.is_registered_by;
        }
        return reg_source;
    },

    adjust_percentage_optouts: function(im, env) {
        var m_store = im.config.metric_store;
        return Q.all([
            go.utils.get_kv(im, [m_store, env, 'sum', 'subscriptions'].join('.'), 0),
            go.utils.get_kv(im, [m_store, env, 'sum', 'optouts'].join('.'), 0),
            go.utils.get_kv(im, [m_store, env, 'sum', 'optout_cause', 'non_loss'].join('.'), 0),
            go.utils.get_kv(im, [m_store, env, 'sum', 'optout_cause', 'loss'].join('.'), 0),
            go.utils.get_kv(im, [m_store, env, 'optout', 'sum', 'subscription_to_protocol_success'].join('.'), 0)
        ]).spread(function(total_subscriptions, total_optouts, non_loss_optouts, loss_optouts, loss_msg_signups) {
            var percentage_optouts = parseFloat(((total_optouts/total_subscriptions)*100).toFixed(2));
            var percentage_non_loss_optouts = parseFloat(((non_loss_optouts / total_subscriptions) * 100).toFixed(2));
            var percentage_loss_msg_signups = parseFloat(((loss_msg_signups / loss_optouts) * 100).toFixed(2));
            return Q.all([
                im.metrics.fire.last([env, 'percent', 'optout', 'all'].join('.'), percentage_optouts),
                im.metrics.fire.last([env, 'percent', 'optout', 'non_loss'].join('.'), percentage_non_loss_optouts),
                im.metrics.fire.last([env, 'percent', 'optout', 'loss', 'msgs'].join('.'), percentage_loss_msg_signups)
            ]);
        });
    },

    loss_message_opt_in: function(im, contact, metric_prefix, env, opts) {
        return Q.all([
            // ensure user is not opted out
            go.utils.opt_in(im, contact),
            // activate new subscription
            go.utils.post_subscription(contact, im, metric_prefix, env, opts),
            // send new subscription info to jembi
            go.utils.jembi_babyloss_send_json(contact, contact, 'babyloss', im, metric_prefix)
        ]);
    },

    opt_out: function(im, contact, optout_reason, api_optout, unsub_all, jembi_optout,
                      metric_prefix, env) {
        var queue1 = [];
        var prior_opt_out_reason;

        // Start Queue 1
        if (optout_reason !== undefined) {
            prior_opt_out_reason = contact.extra.opt_out_reason || 'unknown';
              // if reason was not previously saved it should be 'unknown' (from smsinbound)
            contact.extra.opt_out_reason = optout_reason;
            queue1.push(function() {
                return im.contacts.save(contact);
            });
        }
        // End Queue 1

        return Q
            .all(queue1.map(Q.try))
            .then(function() {
                return go.utils
                    .opted_out(im, contact)
                    .then(function(opted_out) {
                        // if the contact is not opted out, opt them out OR
                        // if the contact has opted out, but has an opted-out reason 'unknown'
                        // (through SMSing STOP) but is now dialing in to opt-out line and
                        // supplying a reason for their optout, opt them out again
                        if (opted_out === false || (prior_opt_out_reason === 'unknown'
                          && im.config.name.substring(0,6) === "optout")) {
                            var queue2 = [];

                            // Start Queue 2
                            if (api_optout === true) {
                                // vumi optout
                                queue2.push(function() {
                                    return im.api_request('optout.optout', {
                                        address_type: "msisdn",
                                        address_value: contact.msisdn,
                                        message_id: im.msg.message_id
                                    });
                                });
                            }

                            if (unsub_all === true) {
                                // deactivate all subscriptions
                                queue2.push(function() {
                                    return go.utils.subscription_unsubscribe_all(contact, im);
                                });
                            }

                            if (jembi_optout === true) {
                                // send optout to jembi
                                queue2.push(function() {
                                    return go.utils.jembi_optout_send_json(contact, contact,
                                      'optout', im, metric_prefix);
                                });

                                // fire opt-out registration source metric
                                var reg_source = go.utils.get_reg_source(contact);
                                queue2.push(function() {
                                    return im.metrics.fire.inc([env, 'sum', 'optout_on',
                                      reg_source].join('.'), {amount: 1});
                                });

                                // fire sum of all opt-outs metric
                                queue2.push(function() {
                                    return im.metrics.fire.inc([env, 'sum', 'optouts'].join('.'),
                                      {amount: 1});
                                });

                                // fire loss / non-loss metric
                                var loss_causes = ['miscarriage', 'babyloss', 'stillbirth'];
                                if (_.contains(loss_causes, contact.extra.opt_out_reason)) {
                                    queue2.push(function() {
                                        return im.metrics.fire.inc([env, 'sum', 'optout_cause',
                                          'loss'].join('.'), {amount: 1});
                                    });
                                } else {
                                    queue2.push(function() {
                                        return im.metrics.fire.inc([env, 'sum', 'optout_cause',
                                          'non_loss'].join('.'), {amount: 1});
                                    });
                                }

                                // fire cause metric
                                queue2.push(function() {
                                    return im.metrics.fire.inc([env, 'sum', 'optout_cause',
                                      optout_reason].join('.'), {amount: 1});
                                });

                            }
                            // End Queue 2

                            return Q
                                .all(queue2.map(Q.try))
                                .then(function() {
                                    return go.utils.adjust_percentage_optouts(im, env);
                                });
                        } else {
                            return Q();
                        }
                    });
            });
    },

    nurse_optout: function(im, contact, optout_reason, api_optout, unsub_all, jembi_optout,
                      patch_last_reg, metric_prefix, env) {
        var queue1 = [];
        var prior_opt_out_reason;

        // Start Queue 1
        if (optout_reason !== undefined) {
            prior_opt_out_reason = contact.extra.nc_opt_out_reason || 'unknown';
              // if reason was not previously saved it should be 'unknown' (from smsinbound)
            contact.extra.nc_opt_out_reason = optout_reason;
            queue1.push(function() {
                return im.contacts.save(contact);
            });
        }
        // End Queue 1

        return Q
            .all(queue1.map(Q.try))
            .then(function() {
                return go.utils
                    .opted_out(im, contact)
                    .then(function(opted_out) {
                        // if the contact is not opted out, opt them out OR
                        // if the contact has opted out, but has an opted-out reason 'unknown'
                        // (through SMSing STOP) but is now dialing in to opt-out line and
                        // supplying a reason for their optout, opt them out again
                        if (opted_out === false || (prior_opt_out_reason === 'unknown'
                          && im.config.name.substring(0,10) === "nurse_ussd")) {
                            var queue2 = [];

                            // Start Queue 2
                            if (api_optout === true) {
                                // vumi optout
                                queue2.push(function() {
                                    return im.api_request('optout.optout', {
                                        address_type: "msisdn",
                                        address_value: contact.msisdn,
                                        message_id: im.msg.message_id
                                    });
                                });
                            }

                            if (unsub_all === true) {
                                // deactivate all subscriptions
                                queue2.push(function() {
                                    return go.utils.subscription_unsubscribe_all(contact, im);
                                });
                            }

                            if (jembi_optout === true) {
                                // send optout to jembi
                                queue2.push(function() {
                                    return go.utils.jembi_nurse_optout_send_json(contact, contact,
                                      'nurse_optout', im, metric_prefix);
                                });

                                // Add Metrics
                                // fire total source opt-outs metric (last)
                                queue2.push(function() {
                                    return im.metrics.fire.inc(
                                        [metric_prefix, 'optouts', 'last'].join('.'),
                                        {amount: 1});
                                });
                                // fire total source opt-outs metric (sum)
                                queue2.push(function() {
                                    return im.metrics.fire.sum(
                                        [metric_prefix, 'optouts', 'sum'].join('.'), 1);
                                });
                                // fire total opt-outs metric (last)
                                queue2.push(function() {
                                    return im.metrics.fire.inc(
                                        [env, 'nurseconnect', 'optouts', 'last'].join('.'),
                                        {amount: 1});
                                });
                                // fire total opt-outs metric (sum)
                                queue2.push(function() {
                                    return im.metrics.fire.sum(
                                        [env, 'nurseconnect', 'optouts', 'sum'].join('.'), 1);
                                });
                                // fire opt-out reason metrics (last)
                                queue2.push(function() {
                                    return im.metrics.fire.inc(
                                        [env, 'nurseconnect', 'optouts', optout_reason, 'last']
                                        .join('.'), {amount: 1});
                                });
                                // fire opt-out reason metrics (sum)
                                queue2.push(function() {
                                    return im.metrics.fire.inc(
                                        [env, 'nurseconnect', 'optouts', optout_reason, 'sum']
                                        .join('.'), 1);
                                });

                            }

                            if (patch_last_reg === true) {
                                // patch last registration to show opted out
                                queue2.push(function() {
                                    return go.utils.patch_last_reg(contact, im, optout_reason);
                                });
                            }
                            // End Queue 2
                            return Q.all(queue2.map(Q.try));
                        } else {
                            return Q();
                        }
                    });
            });
    },

    opted_out: function(im, contact) {
        return im
          .api_request('optout.status', {
              address_type: "msisdn",
              address_value: contact.msisdn
          })
          .then(function(result) {
              return result.opted_out;
          });
    },

    opted_out_by_msisdn: function(im, msisdn) {
        return im.contacts
          .get(msisdn, {create: true})
          .then(function(contact) {
              return go.utils.opted_out(im, contact);
          });
    },

    opt_in: function(im, contact) {
        contact.extra.opt_out_reason = '';

        return Q.all([
            im.api_request('optout.cancel_optout', {
                address_type: "msisdn",
                address_value: contact.msisdn
            }),
            im.contacts.save(contact)
        ]);
    },

    nurse_opt_in: function(im, contact) {
        contact.extra.nc_opt_out_reason = '';
        return Q.all([
            im.api_request('optout.cancel_optout', {
                address_type: "msisdn",
                address_value: contact.msisdn
            }),
            im.contacts.save(contact)
        ]);
    },

    attach_session_length_helper: function (im) {
      // If we have transport metadata then attach the session length
      // helper to this app
      if(!im.msg.transport_metadata)
        return;

      var slh = new go.SessionLengthHelper(im, {
          name: function () {
              var metadata = im.msg.transport_metadata.aat_ussd;
              var provider;
              if(metadata) {
                provider = (metadata.provider || 'unspecified').toLowerCase();
              } else {
                provider = 'unknown';
              }
              return [im.config.name, provider].join('.');
          },
          clock: function () {
              return go.utils.get_today(im.config);
          }
      });
      slh.attach();
      return slh;
    }
};

go.SessionLengthHelper = function () {

  var vumigo = require('vumigo_v02');
  var events = vumigo.events;
  var Eventable = events.Eventable;

  var SessionLengthHelper = Eventable.extend(function(self, im, params) {
    /**class:SessionLengthHelper

    A helper for common session length calculation tasks.

    :param InteractionMachine im:
      The interaction machine that the metrics should be run on.
    :param object params:
      Optional parameters:

      {
        name: 'default',
        clock: function () {
          return new Date();
        },
        metrics_prefix: 'session_length_helper'
      }

    */
    self.im = im;

    self.user = im.user;

    self.name = params.name || 'default';

    self.now = params.clock || function () { return new Date(); };

    self.metrics_prefix = params.metrics_prefix || 'session_length_helper';

    self.mark = {};

    self.attach = function () {
      self.im.on('session:new', function (e) {
        return self.mark.session_start();
      });

      self.im.on('session:close', function (e) {
        return self.mark.session_close();
      });

      self.im.on('im:shutdown', function() {
        return self.increment_and_fire(self.name);
      });
    };

    self.mark.session_start = function () {
      self.user.metadata.session_length_helper = {};
      self.user.metadata.session_length_helper.start = Number(self.now());
      return self;
    };

    self.mark.session_close = function () {
      if(!self.user.metadata.session_length_helper) {
        self.user.metadata.session_length_helper = {};
      }
      self.user.metadata.session_length_helper.stop = Number(self.now());
      return self;
    };

    self.duration = function() {
      var data = self.user.metadata.session_length_helper;
      if(data && data.stop && data.start) {
        return data.stop - data.start;
      }
      return -1;
    };

    self.get_today_as_string = function() {
      var today_iso = self.now().toISOString();
      return today_iso.split('T')[0];
    };

    self.ensure_today = function (name) {
      var sentinel_key_name = [self.metrics_prefix, name, 'sentinel'].join('.');
      return self.im
        .api_request('kv.get', {
          key: sentinel_key_name
        })
        .then(function (result) {
          if(result.value != self.get_today_as_string()) {
            return self.reset_for_today(name);
          }
        });
    };

    self.reset_for_today = function (name) {
      var sentinel_key_name = [self.metrics_prefix, name, 'sentinel'].join('.');
      var key_name = [self.metrics_prefix, name].join('.');
      return self.im
        .api_request('kv.set', {
          key: key_name,
          value: 0
        })
        .then(function (result) {
          return self.im.api_request('kv.set', {
            key: sentinel_key_name,
            value: self.get_today_as_string()
          });
        });
    };

    self.store = function(name) {
      return self.im
        .api_request('kv.incr', {
          key: [self.metrics_prefix, name].join('.'),
          amount: self.duration()
        })
        .then(function (result){
          return result.value;
        });
    };

    self.fire_metrics = function (name, result) {
      var full_name = [self.metrics_prefix, name].join('.');
      return self.im.metrics.fire.max(full_name, result / 1000);
    };

    self.increment_and_fire = function (fn_or_str) {
      var name = vumigo.utils.maybe_call(fn_or_str, self);
      return self
        .ensure_today(name)
        .then(function (result) {

          // return early if we've got nothing to report
          if(self.duration() < 0)
            return;

          return self
            .store(name)
            .then(function (result) {
              return self.fire_metrics(name, result);
            });
        });
    };

  });

  return SessionLengthHelper;

}();

go.app = function() {
    var vumigo = require('vumigo_v02');
    var Q = require('q');
    var App = vumigo.App;
    var EndState = vumigo.states.EndState;

    var GoNDOH = App.extend(function(self) {
        App.call(self, 'states_start');
        var $ = self.$;

        self.init = function() {
            self.env = self.im.config.env;
            self.metric_prefix = [self.env, self.im.config.name].join('.');
            self.store_name = [self.env, self.im.config.name].join('.');

            go.utils.attach_session_length_helper(self.im);

            return self.im.contacts
                .for_user()
                .then(function(user_contact) {
                   self.contact = user_contact;
                });
        };

        self.states.add('states_start', function() {
            // fire inbound message count metric
            return Q.all([
                self.im.metrics.fire.sum(
                    ([self.metric_prefix, "inbound_sms", "sum"].join('.')), 1),
                self.im.metrics.fire.inc(
                    ([self.metric_prefix, "inbound_sms", "last"].join('.')), {amount: 1})
            ]).then(function() {
                // check if message contains a ussd code
                if (self.im.msg.content.indexOf('*120*') > -1 || self.im.msg.content.indexOf('*134*') > -1) {
                    return self.states.create("states_dial_not_sms");
                } else {
                    // get the first word, remove non-alphanumerics, capitalise
                    switch (self.im.msg.content.split(" ")[0].replace(/\W/g, '').toUpperCase()) {
                        case "STOP":
                            return self.states.create("states_opt_out_enter");
                        case "BLOCK":
                            return self.states.create("states_opt_out_enter");
                        case "START":
                            return self.states.create("states_opt_in_enter");
                        default:
                            return self.states.create("st_unrecognised");
                    }
                }
            });
        });

        self.states.add('states_dial_not_sms', function(name) {
            return new EndState(name, {
                text: $("Please use your handset's keypad to dial the number that you received, " +
                        "rather than sending it to us in an sms."),

                next: 'states_start',
            });
        });

        self.states.add('states_opt_out_enter', function(name) {
            return go.utils
                .nurse_optout(self.im, self.contact, optout_reason='unknown', api_optout=true,
                    unsub_all=true, jembi_optout=true, last_reg_patch=true, self.metric_prefix, self.env)
                .then(function() {
                    return self.states.create('states_opt_out');
                });
        });

        self.states.add('states_opt_out', function(name) {
            return new EndState(name, {
                text: $('Thank you. You will no longer receive messages from us.'),
                next: 'states_start'
            });
        });

        self.states.add('states_opt_in_enter', function(name) {
            return go.utils
                .nurse_opt_in(self.im, self.contact)
                .then(function() {
                    return self.states.create('states_opt_in');
                });
        });

        self.states.add('states_opt_in', function(name) {
            return new EndState(name, {
                text: $('Thank you. You will now receive messages from us again. ' +
                        'If you have any medical concerns please visit your nearest clinic'),

                next: 'states_start'
            });
        });

        self.states.add('st_unrecognised', function(name) {
            return new EndState(name, {
                text: $("We do not recognise the message you sent us. Reply STOP " +
                        "to unsubscribe or dial {{channel}} for more options.")
                    .context({channel: self.im.config.nurse_ussd_channel}),
                next: 'states_start'
            });
        });

    });

    return {
        GoNDOH: GoNDOH
    };
}();

go.init = function() {
    var vumigo = require('vumigo_v02');
    var InteractionMachine = vumigo.InteractionMachine;
    var GoNDOH = go.app.GoNDOH;

    return {
        im: new InteractionMachine(api, new GoNDOH())
    };
}();


text:
namespace GeneticAlgorithm.Info
{
    public class Data
    {
        public readonly double X, Y, LeftDistance, ForwardDistance, RightDistance, Degree;
        public double DegreeNormalize => (Degree + 40) / 80;

        public Data(double x, double y, double forward, double left, double right, double degree)
        {
            X = x;
            Y = y;
            LeftDistance = left;
            ForwardDistance = forward;
            RightDistance = right;
            Degree = degree;
        }
    }
}

redpajama_set_name: RedPajamaGithub


text:
@interface MethodTestBean : NSObject
+ (NSString *)classMethod1;
+ (NSString *)classMethod2;
- (NSString *)instanceMethod1:(NSString *)param;
- (NSString *)instanceMethod2:(NSString *)param;

- (void)method0;
- (void)method1:(NSString *)param1;
- (void)method2:(NSString *)param1 param:(NSString *)param2;
- (void)method3:(NSString *)param1 param:(NSString *)param2 param:(NSUInteger)param3;

- (BOOL)method:(BOOL)param;
@end
@implementation MethodTestBean
+ (NSString *)classMethod1 { return @"classMethod1"; }
+ (NSString *)classMethod2 { return @"classMethod2"; }
- (NSString *)instanceMethod1:(NSString *)param { return @"instanceMethod1"; }
- (NSString *)instanceMethod2:(NSString *)param { return @"instanceMethod2"; }

- (void)method0 {}
- (void)method1:(NSString *)param1 {}
- (void)method2:(NSString *)param1 param:(NSString *)param2 {}
- (void)method3:(NSString *)param1 param:(NSString *)param2 param:(NSUInteger)param3 {}

- (BOOL)method:(BOOL)param { return param; }
@end

@interface AGXMethodTest : XCTestCase

@end

@implementation AGXMethodTest

- (void)testAGXMethod {
    AGXMethod *method1 = [AGXMethod classMethodWithName:@"classMethod1" inClassNamed:@"MethodTestBean"];
    AGXMethod *method2 = [AGXMethod classMethodWithName:@"classMethod2" inClassNamed:@"MethodTestBean"];
    AGXMethod *method3 = [AGXMethod classMethodWithName:@"classMethod3" inClassNamed:@"MethodTestBean"];
    XCTAssertEqualObjects(method1.selectorName, @"classMethod1");
    XCTAssertEqualObjects(method2.selectorName, @"classMethod2");
    XCTAssertNil(method3);
    IMP imp1 = method1.implementation;
    IMP imp2 = method2.implementation;
    [method1 setImplementation:imp2];
    [method2 setImplementation:imp1];
    XCTAssertEqualObjects(MethodTestBean.classMethod1, @"classMethod2");
    XCTAssertEqualObjects(MethodTestBean.classMethod2, @"classMethod1");

    method1 = [AGXMethod instanceMethodWithName:@"instanceMethod1:" inClassNamed:@"MethodTestBean"];
    method2 = [AGXMethod instanceMethodWithName:@"instanceMethod2:" inClassNamed:@"MethodTestBean"];
    method3 = [AGXMethod instanceMethodWithName:@"instanceMethod3:" inClassNamed:@"MethodTestBean"];
    XCTAssertEqualObjects(method1.selectorName, @"instanceMethod1:");
    XCTAssertEqualObjects(method2.selectorName, @"instanceMethod2:");
    XCTAssertNil(method3);
    imp1 = method1.implementation;
    imp2 = method2.implementation;
    [method1 setImplementation:imp2];
    [method2 setImplementation:imp1];
    XCTAssertEqualObjects([MethodTestBean.instance instanceMethod1:nil], @"instanceMethod2");
    XCTAssertEqualObjects([MethodTestBean.instance instanceMethod2:nil], @"instanceMethod1");
}

- (void)testPurifiedSignature {
    XCTAssertEqualObjects(@"v@:", [AGXMethod instanceMethodWithName:
                                    @"method0" inClassNamed:@"MethodTestBean"].purifiedSignature);
    XCTAssertEqualObjects(@"v@:@", [AGXMethod instanceMethodWithName:
                                    @"method1:" inClassNamed:@"MethodTestBean"].purifiedSignature);
    XCTAssertEqualObjects(@"v@:@@", [AGXMethod instanceMethodWithName:
                                    @"method2:param:" inClassNamed:@"MethodTestBean"].purifiedSignature);
    XCTAssertEqualObjects(@"v@:@@Q", [AGXMethod instanceMethodWithName:
                                    @"method3:param:param:" inClassNamed:@"MethodTestBean"].purifiedSignature);
}

- (void)testRunSelector {
    XCTAssertEqualObjects(@YES, [MethodTestBean.instance performAGXInstanceMethodForName:@"method:" withObject:@YES]);
}

@end


text:
#ifndef __CONFIGS_MICROPENDOUS3_INCLUDE_BOARD_H
#define __CONFIGS_MICROPENDOUS3_INCLUDE_BOARD_H

/****************************************************************************
 * Included Files
 ****************************************************************************/

#include <nuttx/config.h>

/****************************************************************************
 * Pre-Processor Definitions
 ****************************************************************************/
/* Configuration ************************************************************/

/* Clocking *****************************************************************/
/* Assume default CLKDIV8 fuse setting is overridden to CLKDIV1 */

#define BOARD_XTAL_FREQ        16000000         /* 16MHz crystal */
#define BOARD_CPU_CLOCK        BOARD_XTAL_FREQ  /* F_CPU = 16MHz */

/* LED definitions **********************************************************/
/* The Micropendous 3 has no on-board LEDs */

#define LED_STARTED            0
#define LED_HEAPALLOCATE       1
#define LED_IRQSENABLED        2
#define LED_STACKCREATED       3
#define LED_INIRQ              4
#define LED_SIGNAL             5
#define LED_ASSERTION          6
#define LED_PANIC              7

/* Button definitions *******************************************************/
/* SW1 = Connects to AT90USBxx RESET pin and is not available to software.
 * SW2 = Connects (via a pull-up) to PE-2
 */

/****************************************************************************
 * Public Types
 ****************************************************************************/

#ifndef __ASSEMBLY__

/****************************************************************************
 * Inline Functions
 ****************************************************************************/

/****************************************************************************
 * Public Function Prototypes
 ****************************************************************************/

#ifdef __cplusplus
#define EXTERN extern "C"
extern "C" {
#else
#define EXTERN extern
#endif

#undef EXTERN
#ifdef __cplusplus
}
#endif

#endif /* __ASSEMBLY__ */
#endif /* __CONFIGS_MICROPENDOUS3_INCLUDE_BOARD_H */


text:
package notebook.client

import java.io.File

import akka.actor.{Actor, ActorRef, Props}
import notebook.OutputTypes._
import notebook.kernel._
import notebook.util.{CustomResolvers, Deps}
import sbt._

import scala.collection.immutable.Queue
import scala.concurrent.duration._
import scala.util.{Failure => TFailure, Success => TSuccess}

/**
 * @param _initScripts List of scala source strings to be executed during REPL startup.
 * @param customSparkConf Map configuring the notebook (spark configuration).
 * @param compilerArgs Command line arguments to pass to the REPL compiler
 */
class ReplCalculator(
  notebookName:String,
  customLocalRepo: Option[String],
  customRepos: Option[List[String]], // List("mvn", "my-mvn % repo")
  customDeps: Option[List[String]],
  customImports: Option[List[String]],
  customSparkConf: Option[Map[String, String]],
  _initScripts: List[(String, String)],
  compilerArgs: List[String]
) extends Actor with akka.actor.ActorLogging {
  val initScripts = _initScripts ::: List(("end", "\"END INIT\""))

  private val repoRegex = "(?s)^:local-repo\\s*(.+)\\s*$".r
  private val resolverRegex = "(?s)^:remote-repo\\s*(.+)\\s*$".r
  private val authRegex = """(?s)^\s*\(([^\)]+)\)\s*$""".r
  private val credRegex = """"([^"]+)"\s*,\s*"([^"]+)"""".r //"

  private def outputTypesRegex(ctx: String, outputType: String) = {
    s"(?s)^:$ctx\\s*\n(.+)\\s*$$".r → outputType
  }

  private val htmlContext = outputTypesRegex("html", `text/html`)
  private val plainContext = outputTypesRegex("plain", `text/plain`)
  private val markdownContext = outputTypesRegex("markdown", `text/markdown`)
  private val latexContext = outputTypesRegex("latex", `text/latex`)
  private val svgContext = outputTypesRegex("svg", `image/svg+xml`)
  private val pngContext = outputTypesRegex("png", `image/png`)
  private val jpegContext = outputTypesRegex("jpeg", `image/jpeg`)
  private val pdfContext = outputTypesRegex("pdf", `application/pdf`)
  private val javascriptContext = outputTypesRegex("javascript", `application/javascript`)

  private val cpRegex = "(?s)^:cp\\s*(.+)\\s*$".r
  private val dpRegex = "(?s)^:(l?)dp\\s*(.+)\\s*$".r
  private val sqlRegex = "(?s)^:sql(?:\\[([a-zA-Z0-9][a-zA-Z0-9]*)\\])?\\s*(.+)\\s*$".r
  private val shRegex = "(?s)^:sh\\s*(.+)\\s*$".r

  // note: the resolver list is a superset of Spark's list in o.a.spark.deploy.SparkSubmit
  // except that the local ivy repo isn't included
  var resolvers: List[Resolver] = {
    val mavenLocal = Resolver.mavenLocal
    val mavenReleases = sbt.DefaultMavenRepository
    val typesafeReleases = Resolver.typesafeIvyRepo("releases")
    val jCenterReleases = Resolver.jcenterRepo
    val sonatypeReleases = Resolver.sonatypeRepo("releases")
    val spReleases = new MavenRepository("spark-packages", "http://dl.bintray.com/spark-packages/maven/")
    val defaults = mavenLocal :: mavenReleases :: spReleases :: typesafeReleases :: jCenterReleases :: sonatypeReleases :: Nil
    customRepos.getOrElse(List.empty[String]).map(CustomResolvers.fromString).map(_._2) ::: defaults
  }

  var repo: File = customLocalRepo.map(x => new File(x)).getOrElse {
    val tmp = new File(System.getProperty("java.io.tmpdir"))

    val snb = new File(tmp, "spark-notebook")
    if (!snb.exists) snb.mkdirs

    val repo = new File(snb, "repo")
    if (!repo.exists) repo.mkdirs

    val r = new File(repo, java.util.UUID.randomUUID.toString)
    if (!r.exists) r.mkdirs

    r
  }

  def codeRepo = new File(repo, "code")

  val (depsJars, depsScript): (List[String], (String, () => String)) = customDeps.map { d =>
    val customDeps = d.mkString("\n")
    val deps = Deps.script(customDeps, resolvers, repo).toOption.getOrElse(List.empty[String])
    (deps, ("deps", () => {
      s"""
         |val CustomJars = ${deps.mkString("Array(\"", "\",\"", "\")")}
      """.stripMargin
    }))
  }.getOrElse((List.empty[String], ("deps", () => "val CustomJars = Array.empty[String]\n")))

  val ImportsScripts = ("imports", () => customImports.map(_.mkString("\n") + "\n").getOrElse("\n"))

  private var _repl: Option[Repl] = None

  private def repl: Repl = _repl getOrElse {
    val r = new Repl(compilerArgs, depsJars)
    _repl = Some(r)
    r
  }

  // +/- copied of https://github.com/scala/scala/blob/v2.11.4/src%2Flibrary%2Fscala%2Fconcurrent%2Fduration%2FDuration.scala
  final def toCoarsest(d: FiniteDuration): String = {

    def loop(length: Long, unit: TimeUnit, acc: String): String = {

      def coarserOrThis(coarser: TimeUnit, divider: Int) = {
        if (length == divider)
          loop(1, coarser, acc)
        else if (length < divider)
          FiniteDuration(length, unit).toString + " " + acc
        else {
          val _acc = if (length % divider == 0) {
            acc
          } else {
            FiniteDuration(length % divider, unit).toString + " " + acc
          }
          loop(length / divider, coarser, _acc)
        }
      }

      unit match {
        case DAYS => d.toString + " " + acc
        case HOURS => coarserOrThis(DAYS, 24)
        case MINUTES => coarserOrThis(HOURS, 60)
        case SECONDS => coarserOrThis(MINUTES, 60)
        case MILLISECONDS => coarserOrThis(SECONDS, 1000)
        case MICROSECONDS => coarserOrThis(MILLISECONDS, 1000)
        case NANOSECONDS => coarserOrThis(MICROSECONDS, 1000)
      }
    }

    if (d.unit == DAYS || d.length == 0) d.toString()
    else loop(d.length, d.unit, "").trim
  }

  // Make a child actor so we don't block the execution on the main thread, so that interruption can work
  private val executor = context.actorOf(Props(new Actor {
    implicit val ec = context.dispatcher

    private var queue: Queue[(ActorRef, ExecuteRequest)] = Queue.empty

    def eval(b: => String, notify: Boolean = true)(success: => String = "",
      failure: String => String = (s: String) => "Error evaluating " + b + ": " + s) {
      repl.evaluate(b)._1 match {
        case Failure(str) =>
          if (notify) {
            eval( s"""""", notify = false)()
          }
          log.error(failure(str))
        case _ =>
          if (notify) {
            eval( s"""""", notify = false)()
          }
          log.info(success)
      }
    }

    def receive = {
      case "process-next" =>
        log.debug(s"Processing next asked, queue is ${queue.size} length now")
        if (queue.nonEmpty) {
          //queue could be empty if InterruptRequest was asked!
          log.debug("Dequeuing execute request current size: " + queue.size)
          queue = queue.dequeue._2
          queue.headOption foreach { case (ref, er) =>
            log.debug("About to execute request from the queue")
            execute(ref, er)
          }
        }

      case er@ExecuteRequest(_, code) if queue.nonEmpty =>
        log.debug("Enqueuing execute request at: " + queue.size)
        queue = queue.enqueue(sender(), er)

      case er@ExecuteRequest(_, code) =>
        log.debug("Enqueuing execute request at: " + queue.size)
        queue = queue.enqueue(sender(), er)
        log.debug("Executing execute request")
        execute(sender(), er)

      case InterruptRequest =>
        log.debug("Interrupting the spark context")
        val thisSender = sender()
        log.debug("Clearing the queue of size " + queue.size)
        queue = scala.collection.immutable.Queue.empty
        repl.evaluate(
          "sparkContext.cancelAllJobs()",
          msg => {
            thisSender ! StreamResponse(msg, "stdout")
          }
        )
    }

    def execute(sender: ActorRef, er: ExecuteRequest): Unit = {
      val (outputType, newCode) = er.code match {
        case resolverRegex(r) =>
          log.debug("Adding resolver: " + r)
          val (logR, resolver) = CustomResolvers.fromString(r)
          resolvers = resolver :: resolvers
          (`text/plain`, s""" "Resolver added: $logR!" """)

        case repoRegex(r) =>
          log.debug("Updating local repo: " + r)
          repo = new File(r.trim)
          repo.mkdirs
          (`text/plain`, s""" "Repo changed to ${repo.getAbsolutePath}!" """)

        case dpRegex(local, cp) =>
          log.debug(s"Fetching ${if(local == "l") "locally" else ""} deps using repos: " + resolvers.mkString(" -- "))
          val tryDeps = Deps.script(cp, resolvers, repo)

          tryDeps match {
            case TSuccess(deps) =>
              eval( """sparkContext.stop() """)("CP reload processed successfully",
                (str: String) => "Error in :dp: \n%s".format(str)
              )
              val (_r, replay) = repl.addCp(deps)
              _repl = Some(_r)
              preStartLogic()
              replay()
              val newJarList = if (local == "l") {
                  "Nil"
                } else {
                  deps.mkString("List(\"", "\",\"", "\")")
                }
              (`text/html`,
                s"""
                   |//updating deps
                   |globalScope.jars = ($newJarList ::: globalScope.jars.toList).distinct.toArray
                   |//restarting spark
                   |reset()
                   |globalScope.jars.toList
                 """.stripMargin
              )
            case TFailure(ex) =>
              log.error(ex, "Cannot add dependencies")
              (`text/html`, s"""
${ex.getMessage}
 """)
          }

        case cpRegex(cp) =>
          val jars = cp.trim().split("\n").toList.map(_.trim()).filter(_.length > 0)
          repl.evaluate( """sparkContext.stop()""")._1 match {
            case Failure(str) =>
              log.error("Error in :cp: \n%s".format(str))
            case _ =>
              log.info("CP reload processed successfully")
          }
          val (_r, replay) = repl.addCp(jars)
          _repl = Some(_r)
          preStartLogic()
          replay()

          (`text/html`,
            s"""
              |//updating deps
              |globalScope.jars = (${ jars.mkString("List(\"", "\",\"", "\")") } ::: globalScope.jars.toList).distinct.toArray
              |//restarting spark
              |reset()
              |globalScope.jars.toList
            """.stripMargin
          )

        case shRegex(sh) =>
          val ps = "s\"\"\"" + sh.replaceAll("\\s*\\|\\s*", "\" #\\| \"").replaceAll("\\s*&&\\s*", "\" #&& \"") + "\"\"\""
          (`text/plain`, s"""
                            |import sys.process._
                            |$ps !!
              """.stripMargin.trim
            )

        case sqlRegex(n, sql) =>
          log.debug(s"Received sql code: [$n] $sql")
          val qs = "\"\"\""
          val name = Option(n).map(nm => s"@transient val $nm = ").getOrElse("")
          (`text/html`,
            s"""
            import notebook.front.widgets.Sql
            import notebook.front.widgets.Sql._
            ${name}new Sql(sqlContext, s$qs$sql$qs)
            """
            )

        case htmlContext._1(content) =>
          val ctx = htmlContext._2
          val c = content.toString.replaceAll("\"", """)
          (ctx, " scala.xml.XML.loadString(s\"\"\"" + c + "\"\"\") ")

        case plainContext._1(content) =>
          val ctx = plainContext._2
          val c = content.toString.replaceAll("\"", "\\\\\\\"")
          (ctx, " s\"\"\"" + c + "\"\"\" ")

        case markdownContext._1(content) =>
          val ctx = markdownContext._2
          val c = content.toString.replaceAll("\"", "\\\\\\\"")
          (ctx, " s\"\"\"" + c + "\"\"\" ")

        case latexContext._1(content) =>
          val ctx = latexContext._2
          val c = content.toString.replaceAll("\"", "\\\\\\\"")
          (ctx, " s\"\"\"" + c + "\"\"\" ")

        case svgContext._1(content) =>
          val ctx = svgContext._2
          val c = content.toString.replaceAll("\"", """)
          (ctx, " scala.xml.XML.loadString(s\"\"\"" + c + "\"\"\") ")

        case pngContext._1(content) =>
          val ctx = pngContext._2
          (ctx, content.toString)

        case jpegContext._1(content) =>
          val ctx = jpegContext._2
          (ctx, content.toString)

        case pdfContext._1(content) =>
          val ctx = pdfContext._2
          (ctx, content.toString)

        case javascriptContext._1(content) =>
          val ctx = javascriptContext._2
          val c = content.toString //.replaceAll("\"", "\\\"")
          (ctx, " s\"\"\"" + c + "\"\"\" ")

        case whatever => (`text/html`, whatever)
      }
      val start = System.currentTimeMillis
      val thisSelf = self
      val thisSender = sender
      val result = scala.concurrent.Future {
        // this future is required to allow InterruptRequest messages to be received and process
        // so that spark jobs can be killed and the hand given back to the user to refine their tasks
        val result = repl.evaluate(newCode, msg => thisSender ! StreamResponse(msg, "stdout"))
        val d = toCoarsest(Duration(System.currentTimeMillis - start, MILLISECONDS))
        (d, result._1)
      }

      result foreach {
        case (timeToEval, Success(result)) =>
          thisSender ! ExecuteResponse(outputType, result.toString(), timeToEval)
        case (timeToEval, Failure(stackTrace)) =>
          thisSender ! ErrorResponse(stackTrace, incomplete = false)
        case (timeToEval, notebook.kernel.Incomplete) =>
          thisSender ! ErrorResponse("Incomplete (hint: check the parenthesis)", incomplete = true)
      }

      result onComplete {
        _ => thisSelf ! "process-next"
      }
    }
  }))

  def preStartLogic() {
    log.info("ReplCalculator preStart")

    val dummyScript = ("dummy", () => s"""val dummy = ();\n""")
    val SparkHookScript = (
      "class server",
      () => s"""@transient val _5C4L4_N0T3800K_5P4RK_HOOK = "${repl.classServerUri.get}";\n"""
      )

    val nbName = notebookName.replaceAll("\"", "")

    val SparkConfScript = {
      val m = customSparkConf .getOrElse(Map.empty[String, String])
      m .map { case (k, v) =>
        "( \"" + k + "\"  → \"" + v + "\" )"
      }.mkString(",")
    }

    val CustomSparkConfFromNotebookMD = ("custom conf", () => s"""
      |@transient val notebookName = "$nbName"
      |@transient val _5C4L4_N0T3800K_5P4RK_C0NF:Map[String, String] = Map(
      |  $SparkConfScript
      |)\n
      """.stripMargin
    )

    def eval(script: () => String): Unit = {
      val sc = script()
      log.debug("script is :\n" + sc)
      if (sc.trim.length > 0) {
        val (result, _) = repl.evaluate(sc)
        result match {
          case Failure(str) =>
            log.error("Error in init script: \n%s".format(str))
          case _ =>
            if (log.isDebugEnabled) log.debug("\n" + sc)
            log.info("Init script processed successfully")
        }
      } else ()
    }

    val allInitScrips: List[(String, () => String)] = dummyScript :: SparkHookScript :: depsScript :: ImportsScripts :: CustomSparkConfFromNotebookMD :: initScripts.map(
      x => (x._1, () => x._2))
    for ((name, script) <- allInitScrips) {
      log.info(s" INIT SCRIPT: $name")
      eval(script)
    }
  }

  override def preStart() {
    preStartLogic()
    super.preStart()
  }

  override def postStop() {
    log.info("ReplCalculator postStop")
    super.postStop()
  }

  override def preRestart(reason: Throwable, message: Option[Any]) {
    log.info("ReplCalculator preRestart " + message)
    reason.printStackTrace()
    super.preRestart(reason, message)
  }

  override def postRestart(reason: Throwable) {
    log.info("ReplCalculator postRestart")
    reason.printStackTrace()
    super.postRestart(reason)
  }

  def receive = {
    case msgThatShouldBeFromTheKernel =>

      msgThatShouldBeFromTheKernel match {
        case InterruptRequest =>
          executor.forward(InterruptRequest)

        case req@ExecuteRequest(_, code) => executor.forward(req)

        case CompletionRequest(line, cursorPosition) =>
          val (matched, candidates) = repl.complete(line, cursorPosition)
          sender ! CompletionResponse(cursorPosition, candidates, matched)

        case ObjectInfoRequest(code, position) =>
          val completions = repl.objectInfo(code, position)
          val resp = if (completions.length == 0) {
            ObjectInfoResponse(found = false, code, "", "")
          } else {
            ObjectInfoResponse(found = true, code, completions.mkString("\n"), "")
          }

          sender ! resp
      }
  }
}