/* Teensyduino Core Library
 * http://www.pjrc.com/teensy/
 * Copyright (c) 2017 PJRC.COM, LLC.
 *
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the
 * "Software"), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish,
 * distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to
 * the following conditions:
 *
 * 1. The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * 2. If the Software is incorporated into a build system that allows
 * selection among a list of target devices, then similar target
 * devices manufactured by PJRC.COM must be included in the list of
 * target devices and selectable in the same manner.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
 * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
 * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

#include <Arduino.h>

// IntervalTimer based tone.  This allows tone() to share the timers with other
// libraries, rather than permanently hogging one PIT timer even for projects
// which never use tone().  Someday this single-tone implementation might be
// changed to allow multiple simultaneous tones.

static uint32_t tone_toggle_count;
static volatile uint8_t *tone_reg;
static uint8_t tone_pin=255;
static float tone_usec=0.0;
static uint32_t tone_new_count=0;
IntervalTimer tone_timer;

void tone_interrupt(void);

#if defined(KINETISK)
#define TONE_CLEAR_PIN   tone_reg[0] = 1
#define TONE_TOGGLE_PIN  tone_reg[128] = 1
#define TONE_OUTPUT_PIN  tone_reg[384] = 1

#elif defined(KINETISL)
static uint8_t tone_mask;
#define TONE_CLEAR_PIN   tone_reg[0] = tone_mask
#define TONE_TOGGLE_PIN  tone_reg[4] = tone_mask
#define TONE_OUTPUT_PIN  __disable_irq(); tone_reg[12] |= tone_mask; __enable_irq()

#endif

void tone(uint8_t pin, uint16_t frequency, uint32_t duration)
{
	uint32_t count;
	volatile uint32_t *config;
	float usec;

	if (pin >= CORE_NUM_DIGITAL) return;
	if (duration) {
		count = (frequency * duration / 1000) * 2;
		if (!(count & 1)) count++; // always full waveform cycles
	} else {
		count = 0xFFFFFFFD;
	}
	if (frequency < 1) frequency = 1; // minimum is 1 Hz
	usec = (float)500000.0 / (float)frequency;
	config = portConfigRegister(pin);

	// TODO: IntervalTimer really needs an API to disable and enable
	// the interrupt on a single timer.
	__disable_irq();
	if (pin == tone_pin) {
		// changing a pin which is already playing a tone
		if (usec == tone_usec) {
			// same frequency, so just change the duration
			tone_toggle_count = (tone_toggle_count & 1) + count - 1;
		} else {
			// different frequency, reduce duration to only the
			// remainder of its current cycle, and configure for
			// the transition to the new frequency when the
			// current cycle completes
			tone_usec = usec;
			tone_new_count = count;
			tone_toggle_count = (tone_toggle_count & 1);
		}
	} else {
		// if playing on a different pin, immediately stop, even mid-cycle :-(
		if (tone_pin < CORE_NUM_DIGITAL) {
			TONE_CLEAR_PIN; // clear pin
		}
		// configure the new tone to play
		tone_pin = pin;
		tone_reg = portClearRegister(pin);
		#if defined(KINETISL)
		tone_mask = digitalPinToBitMask(pin);
		#endif
		TONE_CLEAR_PIN; // clear pin
		TONE_OUTPUT_PIN; // output mode;
		*config = PORT_PCR_SRE | PORT_PCR_DSE | PORT_PCR_MUX(1);
		tone_toggle_count = count;
		tone_usec = usec;
		tone_timer.begin(tone_interrupt, usec);
	}
	__enable_irq();
}


void tone_interrupt(void)
{
	if (tone_toggle_count) {  // odd = rising edge, even = falling edge
		// not the end
		TONE_TOGGLE_PIN; // toggle
		tone_toggle_count--;
		if (tone_toggle_count == 0xFFFFFFFB) tone_toggle_count = 0xFFFFFFFD;
	} else {
		// this transition completes the tone
		TONE_CLEAR_PIN; // clear
		if (tone_new_count > 0) {
			// begin playing a new tone
			tone_timer.begin(tone_interrupt, tone_usec);
			tone_toggle_count = tone_new_count;
			tone_new_count = 0;
		} else {
			// finished playing
			tone_timer.end();
			tone_pin = 255;
		}
	}
}

void noTone(uint8_t pin)
{
	if (pin >= CORE_NUM_DIGITAL) return;
	__disable_irq();
	if (pin == tone_pin) {
		tone_timer.end();
		TONE_CLEAR_PIN; // clear
		tone_pin = 255;
	}
	__enable_irq();
}